use super::*;
impl<'a> BodyAnalyzer<'a> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn analyze_fn_decl(
&self,
decl: &php_ast::owned::FunctionDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
crate::attributes::check_function_attributes(
decl, self.db, file, source, source_map, all_issues,
);
let fn_name = decl.name.as_deref().unwrap_or("").to_string();
for param in decl.params.iter() {
if let Some(hint) = ¶m.type_hint {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
if let Some(default_expr) = ¶m.default {
check_expr_for_undefined_classes(
default_expr,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
}
if let Some(hint) = &decl.return_type {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
let resolved = lookup_function_node_for_decl(self.db, file.as_ref(), &fn_name);
let fqn = resolved.as_ref().map(|(f, _)| f.clone());
#[allow(clippy::type_complexity)]
let (params, return_ty, template_params, declared_throws): (
Arc<[mir_codebase::FnParam]>,
_,
Vec<_>,
Arc<[Arc<str>]>,
) = match &resolved {
Some((_, storage)) => {
if storage.params.len() == decl.params.len()
&& storage
.params
.iter()
.zip(decl.params.iter())
.all(|(cp, ap)| ap.name.as_deref().unwrap_or("") == cp.name.as_ref())
{
(
Arc::clone(&storage.params),
storage.return_type.as_deref().cloned(),
storage.template_params.clone(),
Arc::from(storage.throws.as_slice()),
)
} else {
(
Arc::from(ast_derived_fn_params(&decl.params)),
None,
vec![],
Arc::from([]),
)
}
}
None => (
Arc::from(ast_derived_fn_params(&decl.params)),
None,
vec![],
Arc::from([]),
),
};
if self.mode == AnalysisMode::Full {
self.emit_missing_fn_types(
decl,
resolved.as_ref().map(|(_, s)| s),
file,
source,
source_map,
all_issues,
);
}
let declared_return = return_ty.clone();
let mut ctx = FlowState::for_method_with_templates(
¶ms,
return_ty,
declared_throws,
None,
None,
None,
crate::body_analysis::is_strict_types_file(source),
false,
true,
Some(&template_params),
);
ctx.is_in_pure_fn = resolved.as_ref().map(|(_, s)| s.is_pure).unwrap_or(false);
seed_param_locations(&mut ctx, &decl.params, source, source_map);
record_param_symbols(all_symbols, file, source, &decl.params, &ctx);
let mut buf = IssueBuffer::new();
let mut sa = StatementsAnalyzer::new(
self.db,
file.clone(),
source,
source_map,
&mut buf,
all_symbols,
self.php_version,
self.mode,
);
ctx.is_generator = body_has_yield(&decl.body.stmts);
sa.analyze_stmts(&decl.body.stmts, &mut ctx);
let inferred = merge_return_types(&sa.return_types);
let body_diverges = ctx.diverges;
drop(sa);
emit_unused_params(¶ms, &ctx, "", file, all_issues);
emit_unused_variables(&ctx, file, all_issues);
all_issues.extend(buf.into_all_issues());
if self.mode == AnalysisMode::Full && !ctx.is_generator {
crate::diagnostics::check_missing_return(
declared_return.as_ref(),
body_diverges,
&decl.body.span,
file,
source,
source_map,
all_issues,
);
}
if let Some(fqn) = fqn {
self.record_function_inference(&fqn, &inferred);
}
}
fn emit_missing_fn_types(
&self,
decl: &php_ast::owned::FunctionDecl,
stored: Option<&Arc<mir_codebase::storage::FunctionDef>>,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
issues: &mut Vec<Issue>,
) {
let fn_name = decl.name.as_deref().unwrap_or("");
let stored_params_match = stored.is_some_and(|s| s.params.len() == decl.params.len());
if decl.return_type.is_none()
&& stored.is_none_or(|s| s.return_type.is_none())
&& !fn_name.is_empty()
{
let span = fn_header_name_span(source, decl);
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MissingReturnType {
fn_name: fn_name.to_string(),
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
for (i, ast_param) in decl.params.iter().enumerate() {
let stored_ty_present = stored_params_match
&& stored.is_some_and(|s| s.params.get(i).is_some_and(|p| p.ty.is_some()));
if ast_param.type_hint.is_none() && !stored_ty_present {
let param_name = ast_param
.name
.as_deref()
.unwrap_or("")
.trim_start_matches('$')
.to_string();
let span = param_name_span(source, ast_param);
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MissingParamType {
fn_name: fn_name.to_string(),
param: param_name,
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
let Some(stored) = stored else { return };
let template_names: Vec<&str> = stored
.template_params
.iter()
.map(|tp| tp.name.as_ref())
.collect();
if let (Some(hint), Some(doc_ty)) = (&decl.return_type, stored.return_type.as_deref()) {
if doc_ty.from_docblock
&& !docblock_type_unresolvable(doc_ty, &template_names)
&& !fn_name.is_empty()
{
let hint_ty = crate::expr::helpers::resolve_named_objects_in_union(
crate::parser::type_from_hint_owned(hint, None),
self.db,
file.as_ref(),
);
if !hint_ty.is_mixed()
&& !doc_ty.is_mixed()
&& docblock_conflicts_with_hint(self.db, doc_ty, &hint_ty)
{
let span = fn_header_name_span(source, decl);
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MismatchingDocblockReturnType {
declared: doc_ty.to_string(),
inferred: hint_ty.to_string(),
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
}
if let Some(doc_ty) = stored.return_type.as_deref().filter(|t| t.from_docblock) {
let span = fn_header_name_span(source, decl);
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
for atomic in &doc_ty.types {
if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
if !template_names.iter().any(|t| *t == fqcn.as_ref())
&& !crate::db::class_exists(self.db, fqcn.as_ref())
{
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::UndefinedDocblockClass {
name: fqcn.to_string(),
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
}
}
let doc = decl
.doc_comment
.as_ref()
.map(|c| crate::parser::DocblockParser::parse(&c.text))
.unwrap_or_default();
{
for ast_param in decl.params.iter() {
let raw_name = ast_param.name.as_deref().unwrap_or_default();
let (Some(hint), Some(doc_raw)) =
(&ast_param.type_hint, doc.get_param_type(raw_name))
else {
continue;
};
let doc_ty = crate::expr::helpers::resolve_named_objects_in_union(
doc_raw.clone(),
self.db,
file.as_ref(),
);
if docblock_type_unresolvable(&doc_ty, &template_names) {
continue;
}
let hint_ty = crate::expr::helpers::resolve_named_objects_in_union(
crate::parser::type_from_hint_owned(hint, None),
self.db,
file.as_ref(),
);
if hint_ty.is_mixed()
|| doc_ty.is_mixed()
|| !docblock_conflicts_with_hint(self.db, &doc_ty, &hint_ty)
{
continue;
}
let param_name = ast_param
.name
.as_deref()
.unwrap_or("")
.trim_start_matches('$')
.to_string();
let span = param_name_span(source, ast_param);
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MismatchingDocblockParamType {
param: param_name,
declared: doc_ty.to_string(),
inferred: hint_ty.to_string(),
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
{
let fn_span = fn_header_name_span(source, decl);
let (fn_line, fn_col_start) =
crate::diagnostics::offset_to_line_col(source, fn_span.start, source_map);
let (fn_line_end, fn_col_end) =
crate::diagnostics::offset_to_line_col(source, fn_span.end, source_map);
for ast_param in decl.params.iter() {
let raw_name = ast_param.name.as_deref().unwrap_or_default();
let Some(doc_raw) = doc.get_param_type(raw_name) else {
continue;
};
let doc_ty = crate::expr::helpers::resolve_named_objects_in_union(
doc_raw.clone(),
self.db,
file.as_ref(),
);
for atomic in &doc_ty.types {
if let mir_types::Atomic::TNamedObject { fqcn, .. } = atomic {
let looks_like_class = !fqcn.contains('-')
&& !fqcn.contains('(')
&& !fqcn.contains("::")
&& !fqcn.contains('.')
&& !fqcn.starts_with(|c: char| c.is_ascii_digit());
let last_segment = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
let is_template = template_names
.iter()
.any(|t| *t == fqcn.as_ref() || *t == last_segment);
if looks_like_class
&& !is_template
&& !crate::db::class_exists(self.db, fqcn.as_ref())
{
issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::UndefinedDocblockClass {
name: fqcn.to_string(),
},
mir_issues::Location {
file: file.clone(),
line: fn_line,
line_end: fn_line_end,
col_start: fn_col_start,
col_end: fn_col_end.max(fn_col_start + 1),
},
));
}
}
}
}
}
}
pub(crate) fn analyze_fn_decl_pure(
&self,
decl: &php_ast::owned::FunctionDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
) -> crate::db::FunctionInferenceResult {
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
self.db.push_ref_loc_frame();
let mut issues: Vec<Issue> = Vec::new();
let mut discarded_symbols: Vec<ResolvedSymbol> = Vec::new();
let fn_name = decl.name.as_deref().unwrap_or("").to_string();
for param in decl.params.iter() {
if let Some(hint) = ¶m.type_hint {
self.check_and_record_type_hint_classes(
hint,
file,
source,
source_map,
&mut issues,
);
}
if let Some(default_expr) = ¶m.default {
check_expr_for_undefined_classes(
default_expr,
self.db,
file,
source,
source_map,
&mut issues,
self.php_version,
);
}
}
if let Some(hint) = &decl.return_type {
self.check_and_record_type_hint_classes(hint, file, source, source_map, &mut issues);
}
let resolved = lookup_function_node_for_decl(self.db, file.as_ref(), &fn_name);
if self.mode == AnalysisMode::Full {
self.emit_missing_fn_types(
decl,
resolved.as_ref().map(|(_, s)| s),
file,
source,
source_map,
&mut issues,
);
}
#[allow(clippy::type_complexity)]
let (params, return_ty, template_params, declared_throws): (
Arc<[mir_codebase::FnParam]>,
_,
Vec<_>,
Arc<[Arc<str>]>,
) = match &resolved {
Some((_, storage))
if storage.params.len() == decl.params.len()
&& storage
.params
.iter()
.zip(decl.params.iter())
.all(|(cp, ap)| ap.name.as_deref().unwrap_or("") == cp.name.as_ref()) =>
{
(
Arc::clone(&storage.params),
storage.return_type.as_deref().cloned(),
storage.template_params.clone(),
Arc::from(storage.throws.as_slice()),
)
}
_ => (
Arc::from(ast_derived_fn_params(&decl.params)),
None,
vec![],
Arc::from([]),
),
};
let mut ctx = FlowState::for_method_with_templates(
¶ms,
return_ty,
declared_throws,
None,
None,
None,
crate::body_analysis::is_strict_types_file(source),
false,
true,
Some(&template_params),
);
seed_param_locations(&mut ctx, &decl.params, source, source_map);
let mut buf = IssueBuffer::new();
let mut sa = StatementsAnalyzer::new(
self.db,
file.clone(),
source,
source_map,
&mut buf,
&mut discarded_symbols,
self.php_version,
self.mode,
);
ctx.is_generator = body_has_yield(&decl.body.stmts);
sa.analyze_stmts(&decl.body.stmts, &mut ctx);
let inferred = merge_return_types(&sa.return_types);
drop(sa);
emit_unused_params(¶ms, &ctx, "", file, &mut issues);
emit_unused_variables(&ctx, file, &mut issues);
issues.extend(buf.into_all_issues());
let ref_locs = self.db.pop_ref_loc_frame();
crate::db::FunctionInferenceResult {
issues,
ref_locs,
return_type: Some(inferred),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn analyze_fn_decl_typed(
&self,
decl: &php_ast::owned::FunctionDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
type_envs: &mut FxHashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
let fn_name = decl.name.as_deref().unwrap_or("").to_string();
for param in decl.params.iter() {
if let Some(hint) = ¶m.type_hint {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
}
if let Some(hint) = &decl.return_type {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
let resolved = lookup_function_node_for_decl(self.db, file.as_ref(), &fn_name);
if self.mode == AnalysisMode::Full {
self.emit_missing_fn_types(
decl,
resolved.as_ref().map(|(_, s)| s),
file,
source,
source_map,
all_issues,
);
}
let fqn = resolved.as_ref().map(|(f, _)| f.clone());
let (params, return_ty, declared_throws): (
Arc<[mir_codebase::FnParam]>,
_,
Arc<[Arc<str>]>,
) = match &resolved {
Some((_, storage)) => {
if storage.params.len() == decl.params.len()
&& storage
.params
.iter()
.zip(decl.params.iter())
.all(|(cp, ap)| ap.name.as_deref().unwrap_or("") == cp.name.as_ref())
{
(
Arc::clone(&storage.params),
storage.return_type.as_deref().cloned(),
Arc::from(storage.throws.as_slice()),
)
} else {
(
Arc::from(ast_derived_fn_params(&decl.params)),
None,
Arc::from([]),
)
}
}
None => (
Arc::from(ast_derived_fn_params(&decl.params)),
None,
Arc::from([]),
),
};
let mut ctx = FlowState::for_function(
¶ms,
return_ty,
declared_throws,
None,
None,
None,
crate::body_analysis::is_strict_types_file(source),
true,
);
ctx.is_in_pure_fn = resolved.as_ref().map(|(_, s)| s.is_pure).unwrap_or(false);
seed_param_locations(&mut ctx, &decl.params, source, source_map);
record_param_symbols(all_symbols, file, source, &decl.params, &ctx);
let mut buf = IssueBuffer::new();
let mut sa = StatementsAnalyzer::new(
self.db,
file.clone(),
source,
source_map,
&mut buf,
all_symbols,
self.php_version,
self.mode,
);
ctx.is_generator = body_has_yield(&decl.body.stmts);
sa.analyze_stmts(&decl.body.stmts, &mut ctx);
let inferred = merge_return_types(&sa.return_types);
drop(sa);
let scope_name = fqn.clone().unwrap_or_else(|| Arc::from(fn_name));
type_envs.insert(
crate::type_env::ScopeId::Function {
file: file.clone(),
name: scope_name,
},
crate::type_env::TypeEnv::new(ctx.vars.clone()),
);
emit_unused_params(¶ms, &ctx, "", file, all_issues);
emit_unused_variables(&ctx, file, all_issues);
all_issues.extend(buf.into_all_issues());
if let Some(fqn) = fqn {
self.record_function_inference(&fqn, &inferred);
}
}
}