use super::StatementsAnalyzer;
use crate::flow_state::FlowState;
use crate::util::param_name_span;
use mir_issues::{Issue, IssueKind, Location};
use mir_types::Name;
use php_ast::ast::PropertyHookKind;
use php_ast::owned::{
ClassDecl, ClassMemberKind, FunctionDecl, Param, PropertyHook, PropertyHookBody,
};
use php_ast::Span;
use std::sync::Arc;
impl<'a> StatementsAnalyzer<'a> {
fn analyze_property_hooks_stmt(
&mut self,
hooks: &[PropertyHook],
property_ty: Option<mir_types::Type>,
fqcn: &Arc<str>,
parent_fqcn: &Option<Arc<str>>,
strict_types: bool,
) {
for hook in hooks {
let params: Vec<mir_codebase::DeclaredParam> =
if hook.params.is_empty() && matches!(hook.kind, PropertyHookKind::Set) {
vec![mir_codebase::DeclaredParam {
closure_this: None,
name: Name::new("value"),
ty: mir_codebase::wrap_param_type(property_ty.clone()),
out_ty: None,
doc_type_raw: None,
doc_type_file: None,
has_default: false,
default_text: None,
is_variadic: false,
is_byref: hook.by_ref,
is_optional: false,
}]
} else {
hook.params
.iter()
.enumerate()
.map(|(i, p)| {
let explicit_ty = p.type_hint.as_ref().map(|hint| {
crate::parser::type_from_hint_owned(hint, Some(fqcn.as_ref()))
});
let ty = explicit_ty.or_else(|| {
if matches!(hook.kind, PropertyHookKind::Set) && i == 0 {
property_ty.clone()
} else {
None
}
});
mir_codebase::DeclaredParam {
closure_this: None,
name: Name::new(
p.name.as_deref().unwrap_or("").trim_start_matches('$'),
),
ty: mir_codebase::wrap_param_type(ty),
out_ty: None,
doc_type_raw: None,
doc_type_file: None,
has_default: p.default.is_some(),
default_text: None,
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
}
})
.collect()
};
let return_ty = match hook.kind {
PropertyHookKind::Get => property_ty.clone(),
PropertyHookKind::Set => Some(mir_types::Type::void()),
};
let mut hook_ctx = FlowState::for_method(
¶ms,
return_ty,
Arc::from([]),
Some(fqcn.clone()),
parent_fqcn.clone(),
Some(fqcn.clone()),
strict_types,
false,
false,
);
hook_ctx.current_method_name = Some(Arc::from(match hook.kind {
PropertyHookKind::Get => "__property_get",
PropertyHookKind::Set => "__property_set",
}));
for p in hook.params.iter() {
if let Some(default) = &p.default {
let mut default_ctx = hook_ctx.clone();
let mut ea = self.expr_analyzer(&default_ctx);
let _ = ea.analyze(default, &mut default_ctx);
}
}
for p in hook.params.iter() {
if let Some(raw) = p.name.as_deref() {
let trimmed = raw.trim_start_matches('$');
let ty = hook_ctx.get_var(trimmed);
self.record_symbol_for_var(param_name_span(self.source, p), trimmed, ty);
}
}
let mut sa = 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.collect_navigation_facts = self.collect_navigation_facts;
sa.collect_resolved_navigation_facts = self.collect_resolved_navigation_facts;
sa.codebase_symbols_only = self.codebase_symbols_only;
sa.record_reference_locations = self.record_reference_locations;
match &hook.body {
PropertyHookBody::Block(block) => {
hook_ctx.is_generator = crate::body_analysis::body_has_yield(&block.stmts);
sa.analyze_stmts(&block.stmts, &mut hook_ctx);
}
PropertyHookBody::Expression(expr) => {
let mut ea = sa.expr_analyzer(&hook_ctx);
let _ = ea.analyze(expr, &mut hook_ctx);
}
PropertyHookBody::Abstract => {}
}
}
}
pub(crate) fn analyze_function_decl_stmt(&mut self, decl: &FunctionDecl, ctx: &mut FlowState) {
for p in decl.params.iter() {
if let Some(default) = &p.default {
let mut ea = self.expr_analyzer(ctx);
let _ = ea.analyze(default, ctx);
}
}
let fn_name = decl.name.as_deref().unwrap_or("").to_string();
let resolve_fn =
|fqn: &str| -> Option<(Vec<mir_codebase::DeclaredParam>, Option<mir_types::Type>)> {
let db = self.db;
let here = crate::db::Fqcn::from_str(db, fqn);
crate::db::find_function(db, here).map(|f| {
(
f.params.to_vec(),
f.return_type.as_ref().map(|t| (**t).clone()),
)
})
};
let (params, return_ty, resolved_fqn) = if let Some(ns) = self.db.file_namespace(&self.file)
{
let fqn = format!("{}\\{}", ns, fn_name);
if let Some(found) = resolve_fn(&fqn) {
(found.0, found.1, Some(fqn))
} else if let Some(found) = resolve_fn(&fn_name) {
(found.0, found.1, Some(fn_name.clone()))
} else {
let ast_params: Vec<mir_codebase::DeclaredParam> = decl
.params
.iter()
.map(|p| mir_codebase::DeclaredParam {
closure_this: None,
name: Name::new(p.name.as_deref().unwrap_or("").trim_start_matches('$')),
ty: None,
out_ty: None,
doc_type_raw: None,
doc_type_file: None,
has_default: p.default.is_some(),
default_text: None,
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
})
.collect();
(ast_params, None, None)
}
} else if let Some(found) = resolve_fn(&fn_name) {
(found.0, found.1, Some(fn_name.clone()))
} else {
let ast_params: Vec<mir_codebase::DeclaredParam> = decl
.params
.iter()
.map(|p| mir_codebase::DeclaredParam {
closure_this: None,
name: Name::new(p.name.as_deref().unwrap_or("").trim_start_matches('$')),
ty: None,
out_ty: None,
doc_type_raw: None,
doc_type_file: None,
has_default: p.default.is_some(),
default_text: None,
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
})
.collect();
(ast_params, None, None)
};
let mut fn_ctx = FlowState::for_function(
¶ms,
return_ty,
Arc::from([]),
None,
None,
None,
ctx.strict_types,
true,
);
fn_ctx.current_function_fqn = resolved_fqn.map(|fqn| Arc::from(fqn.as_str()));
for p in decl.params.iter() {
if let Some(default) = &p.default {
let mut default_ctx = ctx.clone();
let mut ea = self.expr_analyzer(&default_ctx);
let _ = ea.analyze(default, &mut default_ctx);
self.check_param_default_range(&fn_name, p, ¶ms);
}
}
for p in decl.params.iter() {
if let Some(raw) = p.name.as_deref() {
let trimmed = raw.trim_start_matches('$');
let ty = fn_ctx.get_var(trimmed);
self.record_symbol_for_var(param_name_span(self.source, p), trimmed, ty);
}
}
let mut sa = 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.collect_navigation_facts = self.collect_navigation_facts;
sa.collect_resolved_navigation_facts = self.collect_resolved_navigation_facts;
sa.codebase_symbols_only = self.codebase_symbols_only;
sa.record_reference_locations = self.record_reference_locations;
sa.analyze_stmts(&decl.body.stmts, &mut fn_ctx);
}
pub(crate) fn analyze_class_decl_stmt(&mut self, decl: &ClassDecl, ctx: &mut FlowState) {
let class_name = decl
.name
.as_ref()
.and_then(|i| i.as_deref())
.unwrap_or("<anonymous>");
let resolved = crate::db::resolve_name(self.db, &self.file, class_name);
let fqcn: Arc<str> = Arc::from(resolved.as_str());
let here = crate::db::Fqcn::from_str(self.db, fqcn.as_ref());
let parent_fqcn = if decl.name.as_ref().and_then(|i| i.as_deref()).is_some() {
crate::db::find_class_like(self.db, here).and_then(|c| c.parent().cloned())
} else {
decl.extends.as_ref().map(|parent_name| {
let parent_str = crate::parser::name_to_string_owned(parent_name);
let resolved_parent = crate::db::resolve_name(self.db, &self.file, &parent_str);
Arc::from(resolved_parent.as_str())
})
};
let mut param_default_ctx = ctx.clone();
param_default_ctx.self_fqcn = Some(fqcn.clone());
param_default_ctx.parent_fqcn = parent_fqcn.clone();
param_default_ctx.static_fqcn = Some(fqcn.clone());
if let Some(parent) = &decl.extends {
let parent_str = crate::parser::name_to_string_owned(parent);
let parent_resolved = crate::db::resolve_name(self.db, &self.file, &parent_str);
if !ctx.is_class_guarded(parent_resolved.as_str()) {
self.check_name_undefined_class(parent);
}
self.record_class_like_ref(&parent_resolved, parent.span);
self.check_extends_final_class(&parent_resolved, &fqcn, parent.span);
}
for iface in decl.implements.iter() {
let iface_str = crate::parser::name_to_string_owned(iface);
let iface_resolved = crate::db::resolve_name(self.db, &self.file, &iface_str);
if !ctx.is_class_guarded(iface_resolved.as_str()) {
self.check_name_undefined_class(iface);
}
self.record_class_like_ref(&iface_resolved, iface.span);
}
for member in decl.body.members.iter() {
if let ClassMemberKind::TraitUse(tu) = &member.kind {
for trait_name in tu.traits.iter() {
let trait_str = crate::parser::name_to_string_owned(trait_name);
let trait_resolved = crate::db::resolve_name(self.db, &self.file, &trait_str);
if !ctx.is_class_guarded(trait_resolved.as_str()) {
self.check_name_undefined_trait(trait_name);
}
self.record_class_like_ref(&trait_resolved, trait_name.span);
}
}
}
for member in decl.body.members.iter() {
if let ClassMemberKind::Property(prop) = &member.kind {
if let Some(default) = &prop.default {
let mut ea = self.expr_analyzer(¶m_default_ctx);
let _ = ea.analyze(default, &mut param_default_ctx);
self.check_property_default_range(&fqcn, prop.name.as_deref(), default);
}
if !prop.hooks.is_empty() {
let property_ty = crate::db::find_property_in_class(
self.db,
crate::db::Fqcn::from_str(self.db, fqcn.as_ref()),
prop.name.as_deref().unwrap_or(""),
)
.and_then(|def| def.ty.as_deref().cloned())
.or_else(|| {
prop.type_hint.as_ref().map(|hint| {
crate::parser::type_from_hint_owned(hint, Some(fqcn.as_ref()))
})
});
self.analyze_property_hooks_stmt(
&prop.hooks,
property_ty,
&fqcn,
&parent_fqcn,
ctx.strict_types,
);
}
continue;
}
let ClassMemberKind::Method(method) = &member.kind else {
continue;
};
let method_name = method.name.as_deref().unwrap_or("");
let declared_params = crate::db::find_method_in_chain(
self.db,
crate::db::Fqcn::from_str(self.db, fqcn.as_ref()),
method_name,
)
.map(|(_, storage)| storage.params.to_vec())
.unwrap_or_default();
for p in method.params.iter() {
if let Some(default) = &p.default {
let mut ea = self.expr_analyzer(¶m_default_ctx);
let _ = ea.analyze(default, &mut param_default_ctx);
self.check_param_default_range(
&format!("{fqcn}::{method_name}"),
p,
&declared_params,
);
}
}
if method_name == "__construct" {
for p in method.params.iter() {
if p.visibility.is_none() || p.hooks.is_empty() {
continue;
}
let property_name = p.name.as_deref().unwrap_or("").trim_start_matches('$');
let property_ty = crate::db::find_property_in_class(
self.db,
crate::db::Fqcn::from_str(self.db, fqcn.as_ref()),
property_name,
)
.and_then(|def| def.ty.as_deref().cloned())
.or_else(|| {
p.type_hint.as_ref().map(|hint| {
crate::parser::type_from_hint_owned(hint, Some(fqcn.as_ref()))
})
});
self.analyze_property_hooks_stmt(
&p.hooks,
property_ty,
&fqcn,
&parent_fqcn,
ctx.strict_types,
);
}
}
let Some(body) = &method.body else { continue };
let pulled = crate::db::find_method_in_chain(
self.db,
crate::db::Fqcn::from_str(self.db, fqcn.as_ref()),
method_name,
);
let (params, return_ty) = if let Some((_, storage)) = pulled {
(
storage.params.to_vec(),
storage.return_type.as_deref().cloned(),
)
} else {
let ast_params = method
.params
.iter()
.map(|p| mir_codebase::DeclaredParam {
closure_this: None,
name: Name::new(p.name.as_deref().unwrap_or("").trim_start_matches('$')),
ty: None,
out_ty: None,
doc_type_raw: None,
doc_type_file: None,
has_default: p.default.is_some(),
default_text: None,
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
})
.collect();
let ret = method
.return_type
.as_ref()
.map(|h| crate::parser::type_from_hint_owned(h, Some(fqcn.as_ref())));
(ast_params, ret)
};
let is_ctor = method_name == "__construct";
let mut method_ctx = FlowState::for_method(
¶ms,
return_ty,
Arc::from([]),
Some(fqcn.clone()),
parent_fqcn.clone(),
Some(fqcn.clone()),
ctx.strict_types,
is_ctor,
method.is_static,
);
method_ctx.current_method_name = Some(Arc::from(method_name));
for p in method.params.iter() {
if let Some(raw) = p.name.as_deref() {
let trimmed = raw.trim_start_matches('$');
let ty = method_ctx.get_var(trimmed);
self.record_symbol_for_var(param_name_span(self.source, p), trimmed, ty);
}
}
let mut sa = 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(&body.stmts, &mut method_ctx);
}
}
fn add_issue_at(&mut self, kind: IssueKind, span: Span) {
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = self.offset_to_line_col(span.end);
self.issues.add(Issue::new(
kind,
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
},
));
}
pub(crate) fn check_param_default_range(
&mut self,
fn_name: &str,
p: &Param,
declared_params: &[mir_codebase::DeclaredParam],
) {
let (Some(default), Some(raw)) = (&p.default, p.name.as_deref()) else {
return;
};
let name = raw.trim_start_matches('$');
let Some(declared) = declared_params
.iter()
.find(|d| d.name.as_str() == name)
.and_then(|d| d.ty.as_deref())
else {
return;
};
if crate::contradiction::int_default_outside_range(declared, default) {
let actual = self.source[default.span.start as usize..default.span.end as usize]
.trim()
.to_string();
self.add_issue_at(
IssueKind::InvalidArgument {
param: name.to_string(),
fn_name: fn_name.to_string(),
expected: declared.to_string(),
actual,
},
default.span,
);
}
}
pub(crate) fn check_property_default_range(
&mut self,
fqcn: &str,
prop_name: Option<&str>,
default: &php_ast::owned::Expr,
) {
let Some(name) = prop_name.map(|n| n.trim_start_matches('$')) else {
return;
};
let Some(declared) = crate::db::find_property_in_class(
self.db,
crate::db::Fqcn::from_str(self.db, fqcn),
name,
)
.and_then(|def| def.ty.as_deref().cloned()) else {
return;
};
if crate::contradiction::int_default_outside_range(&declared, default) {
let actual = self.source[default.span.start as usize..default.span.end as usize]
.trim()
.to_string();
self.add_issue_at(
IssueKind::InvalidPropertyAssignment {
property: name.to_string(),
expected: declared.to_string(),
actual,
},
default.span,
);
}
}
fn check_extends_final_class(&mut self, parent_resolved: &str, child_fqcn: &str, span: Span) {
let here = crate::db::Fqcn::from_str(self.db, parent_resolved);
let Some(parent_class) = crate::db::find_class_like(self.db, here) else {
return;
};
if !parent_class.is_final() {
return;
}
let (line, col_start) = self.offset_to_line_col(span.start);
let (line_end, col_end) = self.offset_to_line_col(span.end);
self.issues.add(Issue::new(
IssueKind::InvalidExtendClass {
parent: parent_resolved.to_string(),
child: child_fqcn.to_string(),
},
Location {
file: self.file.clone(),
line,
line_end,
col_start,
col_end: crate::diagnostics::clamp_col_end(line, line_end, col_start, col_end),
},
));
}
}