use rustc_hash::FxHashMap;
use std::sync::Arc;
use mir_issues::Issue;
use mir_types::{Name, Type};
use parking_lot::Mutex;
use crate::db::{resolve_name, MirDatabase};
use crate::diagnostics::{
check_expr_for_undefined_classes, check_name_class, check_type_hint_classes,
collect_type_hint_class_refs, emit_unused_params, emit_unused_variables,
};
use crate::php_version::PhpVersion;
use crate::symbol::ResolvedSymbol;
pub(crate) fn for_each_file_scope_decl<'a>(
stmts: &'a [php_ast::owned::Stmt],
f: &mut dyn FnMut(&'a php_ast::owned::Stmt),
) {
for stmt in stmts.iter() {
visit_file_scope_stmt(stmt, f);
}
}
fn visit_file_scope_stmt<'a>(
stmt: &'a php_ast::owned::Stmt,
f: &mut dyn FnMut(&'a php_ast::owned::Stmt),
) {
use php_ast::owned::{NamespaceBody, StmtKind};
match &stmt.kind {
StmtKind::If(s) => {
visit_file_scope_stmt(&s.then_branch, f);
for branch in s.elseif_branches.iter() {
visit_file_scope_stmt(&branch.body, f);
}
if let Some(else_branch) = &s.else_branch {
visit_file_scope_stmt(else_branch, f);
}
}
StmtKind::While(s) => visit_file_scope_stmt(&s.body, f),
StmtKind::For(s) => visit_file_scope_stmt(&s.body, f),
StmtKind::Foreach(s) => visit_file_scope_stmt(&s.body, f),
StmtKind::DoWhile(s) => visit_file_scope_stmt(&s.body, f),
StmtKind::Switch(s) => {
for case in s.body.cases.iter() {
for inner in case.body.iter() {
visit_file_scope_stmt(inner, f);
}
}
}
StmtKind::TryCatch(t) => {
for inner in t.body.stmts.iter() {
visit_file_scope_stmt(inner, f);
}
for catch in t.catches.iter() {
for inner in catch.body.stmts.iter() {
visit_file_scope_stmt(inner, f);
}
}
if let Some(finally) = &t.finally {
for inner in finally.stmts.iter() {
visit_file_scope_stmt(inner, f);
}
}
}
StmtKind::Block(b) => {
for inner in b.stmts.iter() {
visit_file_scope_stmt(inner, f);
}
}
StmtKind::Declare(d) => {
if let Some(body) = &d.body {
visit_file_scope_stmt(body, f);
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(block) = &ns.body {
for inner in block.stmts.iter() {
visit_file_scope_stmt(inner, f);
}
}
}
_ => f(stmt),
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum AnalysisMode {
Full,
InferenceOnly,
}
#[derive(Clone)]
pub(crate) struct InferredTypes {
pub(crate) functions: Vec<(Arc<str>, Type)>,
pub(crate) methods: Vec<(Arc<str>, Arc<str>, Type)>,
}
#[allow(clippy::type_complexity)]
fn method_chain_signature(
db: &dyn MirDatabase,
fqcn: &str,
method_name: &str,
) -> (
Arc<[mir_codebase::storage::FnParam]>,
Option<Type>,
Vec<mir_codebase::storage::TemplateParam>,
Arc<[Arc<str>]>,
) {
if let Some((_, storage)) =
crate::db::find_method_in_chain(db, crate::db::Fqcn::from_str(db, fqcn), method_name)
{
return (
Arc::clone(&storage.params),
storage.return_type.as_deref().cloned(),
storage.template_params.clone(),
Arc::from(storage.throws.as_slice()),
);
}
(Arc::from([]), None, vec![], Arc::from([]))
}
fn lookup_function_node_for_decl(
db: &dyn MirDatabase,
file: &str,
fn_name: &str,
) -> Option<(Arc<str>, Arc<mir_codebase::storage::FunctionDef>)> {
let qualified = resolve_name(db, file, fn_name);
let try_lookup = |fqn: &str| -> Option<Arc<mir_codebase::storage::FunctionDef>> {
crate::db::find_function(db, crate::db::Fqcn::from_str(db, fqn))
};
if let Some(f) = try_lookup(qualified.as_str()) {
return Some((Arc::from(qualified), f));
}
if let Some(f) = try_lookup(fn_name) {
return Some((Arc::from(fn_name), f));
}
crate::metrics::record_fn_short_name_scan();
for fqn in crate::db::workspace_functions(db).iter() {
let short = fqn.rsplit('\\').next().unwrap_or(fqn.as_ref());
if short == fn_name {
if let Some(f) = try_lookup(fqn.as_ref()) {
return Some((fqn.clone(), f));
}
}
}
None
}
fn ast_derived_fn_params(params: &[php_ast::owned::Param]) -> Vec<mir_codebase::FnParam> {
params
.iter()
.map(|p| mir_codebase::FnParam {
name: Name::new(p.name.as_deref().unwrap_or("")),
ty: None,
has_default: p.default.is_some(),
is_variadic: p.variadic,
is_byref: p.by_ref,
is_optional: p.default.is_some() || p.variadic,
})
.collect()
}
#[derive(Clone, Copy)]
enum DeclKind {
Class,
Interface,
Trait,
Enum,
Function,
}
fn collect_decl_spans(
stmts: &[php_ast::owned::Stmt],
ns_prefix: &str,
out: &mut Vec<(String, php_ast::Span, DeclKind)>,
) {
use php_ast::owned::{NamespaceBody, StmtKind};
let mut current_prefix = ns_prefix.to_string();
for stmt in stmts {
match &stmt.kind {
StmtKind::Class(d) => {
let name = d.name.as_ref().and_then(|n| n.as_deref()).unwrap_or("");
out.push((
format!("{current_prefix}{name}"),
stmt.span,
DeclKind::Class,
));
}
StmtKind::Interface(d) => {
let name = d.name.as_deref().unwrap_or("");
out.push((
format!("{current_prefix}{name}"),
stmt.span,
DeclKind::Interface,
));
}
StmtKind::Trait(d) => {
let name = d.name.as_deref().unwrap_or("");
out.push((
format!("{current_prefix}{name}"),
stmt.span,
DeclKind::Trait,
));
}
StmtKind::Enum(d) => {
let name = d.name.as_deref().unwrap_or("");
out.push((format!("{current_prefix}{name}"), stmt.span, DeclKind::Enum));
}
StmtKind::Function(d) => {
let name = d.name.as_deref().unwrap_or("");
out.push((
format!("{current_prefix}{name}"),
stmt.span,
DeclKind::Function,
));
}
StmtKind::Namespace(ns) => {
let raw_ns = ns
.name
.as_ref()
.map(|n| {
n.parts
.iter()
.map(|p| p.as_ref())
.collect::<Vec<_>>()
.join("\\")
})
.unwrap_or_default();
let child_prefix = if raw_ns.is_empty() {
String::new()
} else {
format!("{raw_ns}\\")
};
match &ns.body {
NamespaceBody::Braced(block) => {
collect_decl_spans(&block.stmts, &child_prefix, out);
}
NamespaceBody::Simple => current_prefix = child_prefix,
}
}
_ => {}
}
}
}
pub(crate) fn check_duplicate_declarations(
stmts: &[php_ast::owned::Stmt],
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
issues: &mut Vec<Issue>,
) {
let mut decls: Vec<(String, php_ast::Span, DeclKind)> = Vec::new();
collect_decl_spans(stmts, "", &mut decls);
let mut seen_class_like: FxHashMap<Name, ()> = FxHashMap::default();
let mut seen_fns: FxHashMap<Name, ()> = FxHashMap::default();
for (fqn, span, kind) in &decls {
let key = Name::from(fqn.as_str()).ascii_lowercase();
let seen = match kind {
DeclKind::Function => &mut seen_fns,
_ => &mut seen_class_like,
};
if let std::collections::hash_map::Entry::Vacant(e) = seen.entry(key) {
e.insert(());
} else {
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);
let issue_kind = match kind {
DeclKind::Class => mir_issues::IssueKind::DuplicateClass { name: fqn.clone() },
DeclKind::Interface => {
mir_issues::IssueKind::DuplicateInterface { name: fqn.clone() }
}
DeclKind::Trait => mir_issues::IssueKind::DuplicateTrait { name: fqn.clone() },
DeclKind::Enum => mir_issues::IssueKind::DuplicateEnum { name: fqn.clone() },
DeclKind::Function => {
mir_issues::IssueKind::DuplicateFunction { name: fqn.clone() }
}
};
issues.push(Issue::new(
issue_kind,
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end,
},
));
}
}
}
pub(crate) struct MethodScopeCx {
pub fqcn: Arc<str>,
pub parent_fqcn: Option<Arc<str>>,
pub detect_ctor: bool,
pub with_templates: bool,
pub check_returns: bool,
pub analyze_param_defaults: bool,
}
pub(crate) struct BodyAnalyzer<'a> {
db: &'a dyn MirDatabase,
php_version: PhpVersion,
mode: AnalysisMode,
inferred_types: Arc<Mutex<InferredTypes>>,
}
impl<'a> BodyAnalyzer<'a> {
pub(crate) fn new(db: &'a dyn MirDatabase, php_version: PhpVersion) -> Self {
Self {
db,
php_version,
mode: AnalysisMode::Full,
inferred_types: Arc::new(Mutex::new(InferredTypes {
functions: Vec::new(),
methods: Vec::new(),
})),
}
}
pub(crate) fn new_inference_only(db: &'a dyn MirDatabase, php_version: PhpVersion) -> Self {
Self {
db,
php_version,
mode: AnalysisMode::InferenceOnly,
inferred_types: Arc::new(Mutex::new(InferredTypes {
functions: Vec::new(),
methods: Vec::new(),
})),
}
}
pub(crate) fn take_inferred_types(&self) -> InferredTypes {
let types = Arc::clone(&self.inferred_types);
Arc::try_unwrap(types)
.map(|mutex| mutex.into_inner())
.unwrap_or_else(|arc| arc.lock().clone())
}
fn record_function_inference(&self, fqn: &Arc<str>, inferred: &Type) {
if self.mode == AnalysisMode::InferenceOnly {
let mut types = self.inferred_types.lock();
types.functions.push((fqn.clone(), inferred.clone()));
}
}
fn record_method_inference(&self, fqcn: &str, name: &str, inferred: &Type) {
if self.mode == AnalysisMode::InferenceOnly {
let mut types = self.inferred_types.lock();
types
.methods
.push((Arc::from(fqcn), Arc::from(name), inferred.clone()));
}
}
fn check_and_record_type_hint_classes(
&self,
hint: &php_ast::owned::TypeHint,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
) {
check_type_hint_classes(
hint,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
if self.mode == AnalysisMode::Full {
for (fqcn, span) in collect_type_hint_class_refs(hint, self.db, file) {
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, span.start, source_map);
let (_, col_end) =
crate::diagnostics::offset_to_line_col(source, span.end, source_map);
self.db.record_reference_location(crate::db::RefLoc {
symbol_key: fqcn,
file: file.clone(),
line,
col_start,
col_end: col_end.max(col_start + 1),
});
}
}
}
pub(crate) fn analyze_bodies(
&self,
program: &php_ast::owned::Program,
file: Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
) -> (Vec<Issue>, Vec<ResolvedSymbol>) {
let mut all_issues = Vec::new();
let mut all_symbols = Vec::new();
if self.mode == AnalysisMode::Full {
check_duplicate_declarations(
&program.stmts,
&file,
source,
source_map,
&mut all_issues,
);
}
self.analyze_top_level_stmts(
&program.stmts,
&file,
source,
source_map,
&mut all_issues,
&mut all_symbols,
);
self.analyze_global_exec(
program,
&file,
source,
source_map,
&mut all_issues,
&mut all_symbols,
);
(all_issues, all_symbols)
}
pub(crate) fn analyze_global_exec(
&self,
program: &php_ast::owned::Program,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use php_ast::owned::StmtKind;
if self.mode != AnalysisMode::Full {
return;
}
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
let mut ctx = FlowState::new();
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,
);
for stmt in program.stmts.iter() {
match &stmt.kind {
StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Enum(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_)
| StmtKind::Namespace(_)
| StmtKind::Use(_) => {}
_ => {
sa.analyze_stmt(stmt, &mut ctx);
}
}
}
drop(sa);
crate::diagnostics::emit_unused_variables(&ctx, file, all_issues);
all_issues.extend(buf.into_issues());
}
pub(crate) fn analyze_bodies_typed(
&self,
program: &php_ast::owned::Program,
file: Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
type_envs: &mut FxHashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
all_symbols: &mut Vec<ResolvedSymbol>,
) -> Vec<Issue> {
use php_ast::owned::StmtKind;
let mut all_issues = Vec::new();
self.analyze_top_level_stmts_typed(
&program.stmts,
&file,
source,
source_map,
&mut all_issues,
type_envs,
all_symbols,
);
{
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
let mut ctx = FlowState::new();
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,
);
for stmt in program.stmts.iter() {
match &stmt.kind {
StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Enum(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_)
| StmtKind::Namespace(_)
| StmtKind::Use(_) => {}
_ => {
sa.analyze_stmt(stmt, &mut ctx);
}
}
}
drop(sa);
crate::diagnostics::emit_unused_variables(&ctx, &file, &mut all_issues);
all_issues.extend(buf.into_issues());
}
all_issues
}
fn analyze_top_level_stmts(
&self,
stmts: &[php_ast::owned::Stmt],
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use php_ast::owned::StmtKind;
for stmt in stmts.iter() {
match &stmt.kind {
StmtKind::Function(decl) => {
self.analyze_fn_decl(decl, file, source, source_map, all_issues, all_symbols);
}
StmtKind::Class(decl) => {
self.analyze_class_decl(
decl,
file,
source,
source_map,
all_issues,
all_symbols,
);
}
StmtKind::Enum(decl) => {
self.analyze_enum_decl(decl, file, source, source_map, all_issues, all_symbols);
}
StmtKind::Interface(decl) => {
self.analyze_interface_decl(decl, file, source, source_map, all_issues);
}
StmtKind::Trait(decl) => {
self.analyze_trait_decl(
decl,
file,
source,
source_map,
all_issues,
all_symbols,
);
}
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
self.analyze_top_level_stmts(
&inner.stmts,
file,
source,
source_map,
all_issues,
all_symbols,
);
}
}
StmtKind::Use(use_decl) => {
check_use_decl_casing(use_decl, self.db, file, source, source_map, all_issues);
}
_ => {}
}
}
}
#[allow(clippy::too_many_arguments)]
fn analyze_top_level_stmts_typed(
&self,
stmts: &[php_ast::owned::Stmt],
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
type_envs: &mut rustc_hash::FxHashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use php_ast::owned::StmtKind;
for stmt in stmts.iter() {
match &stmt.kind {
StmtKind::Function(decl) => {
self.analyze_fn_decl_typed(
decl,
file,
source,
source_map,
all_issues,
type_envs,
all_symbols,
);
}
StmtKind::Class(decl) => {
self.analyze_class_decl_typed(
decl,
file,
source,
source_map,
all_issues,
type_envs,
all_symbols,
);
}
StmtKind::Enum(decl) => {
self.analyze_enum_decl_typed(
decl,
file,
source,
source_map,
all_issues,
type_envs,
all_symbols,
);
}
StmtKind::Interface(decl) => {
self.analyze_interface_decl(decl, file, source, source_map, all_issues);
}
StmtKind::Trait(decl) => {
self.analyze_trait_decl_typed(
decl,
file,
source,
source_map,
all_issues,
type_envs,
all_symbols,
);
}
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
self.analyze_top_level_stmts_typed(
&inner.stmts,
file,
source,
source_map,
all_issues,
type_envs,
all_symbols,
);
}
}
StmtKind::Use(use_decl) => {
check_use_decl_casing(use_decl, self.db, file, source, source_map, all_issues);
}
_ => {}
}
}
}
#[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([]),
),
};
let declared_return = return_ty.clone();
let mut ctx = FlowState::for_method_with_templates(
¶ms,
return_ty,
declared_throws,
None,
None,
None,
false,
false,
true,
Some(&template_params),
);
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_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);
}
}
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);
#[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,
false,
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_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)]
#[allow(clippy::too_many_arguments)]
fn check_property_member(
&self,
prop: &php_ast::owned::PropertyDecl,
member_span: &php_ast::Span,
fqcn: &str,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
) {
if let Some(hint) = &prop.type_hint {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
} else if self.mode == AnalysisMode::Full {
let prop_name = prop.name.as_deref().unwrap_or("").to_string();
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, member_span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, member_span.end, source_map);
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MissingPropertyType {
class: fqcn.to_string(),
property: prop_name,
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
#[allow(clippy::too_many_arguments)]
fn analyze_method_scope(
&self,
method: &php_ast::owned::MethodDecl,
cx: &MethodScopeCx,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<ResolvedSymbol>,
type_envs: Option<&mut FxHashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>>,
) {
use crate::flow_state::FlowState;
use crate::stmt::StatementsAnalyzer;
use mir_issues::IssueBuffer;
let fqcn: &str = cx.fqcn.as_ref();
for param in method.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) = &method.return_type {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
if cx.analyze_param_defaults && method.params.iter().any(|p| p.default.is_some()) {
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,
);
let mut default_ctx = FlowState::new();
default_ctx.self_fqcn = Some(cx.fqcn.clone());
default_ctx.parent_fqcn = cx.parent_fqcn.clone();
default_ctx.static_fqcn = Some(cx.fqcn.clone());
for p in method.params.iter() {
if let Some(default) = &p.default {
let mut ea = sa.expr_analyzer(&default_ctx);
let _ = ea.analyze(default, &mut default_ctx);
}
}
drop(sa);
all_issues.extend(buf.into_issues());
}
let Some(body) = &method.body else { return };
let method_name = method.name.as_deref().unwrap_or("");
let (params, return_ty, template_params, declared_throws) =
method_chain_signature(self.db, fqcn, method_name);
let declared_return = return_ty.clone();
let is_ctor = cx.detect_ctor && method_name == "__construct";
let templates: Option<&[mir_codebase::storage::TemplateParam]> = if cx.with_templates {
Some(&template_params)
} else {
None
};
let mut ctx = FlowState::for_method_with_templates(
¶ms,
return_ty,
declared_throws,
Some(cx.fqcn.clone()),
cx.parent_fqcn.clone(),
Some(cx.fqcn.clone()),
false,
is_ctor,
method.is_static,
templates,
);
seed_param_locations(&mut ctx, &method.params, source, source_map);
record_param_symbols(all_symbols, file, source, &method.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(&body.stmts);
sa.analyze_stmts(&body.stmts, &mut ctx);
let inferred = merge_return_types(&sa.return_types);
let body_diverges = ctx.diverges;
drop(sa);
if let Some(type_envs) = type_envs {
type_envs.insert(
crate::type_env::ScopeId::Method {
class: cx.fqcn.clone(),
method: Arc::from(method_name),
},
crate::type_env::TypeEnv::new(ctx.vars.clone()),
);
}
emit_unused_params(¶ms, &ctx, method_name, file, all_issues);
emit_unused_variables(&ctx, file, all_issues);
all_issues.extend(buf.into_issues());
if cx.check_returns && self.mode == AnalysisMode::Full && !is_ctor && !ctx.is_generator {
crate::diagnostics::check_missing_return(
declared_return.as_ref(),
body_diverges,
&body.span,
file,
source,
source_map,
all_issues,
);
}
if cx.check_returns
&& self.mode == AnalysisMode::Full
&& method_name.eq_ignore_ascii_case("__tostring")
{
crate::diagnostics::check_to_string_return(
fqcn,
declared_return.as_ref(),
&inferred,
&body.span,
file,
source,
source_map,
all_issues,
);
}
self.record_method_inference(fqcn, method_name, &inferred);
}
pub(crate) fn analyze_class_decl(
&self,
decl: &php_ast::owned::ClassDecl,
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_class_attributes(
decl, self.db, file, source, source_map, all_issues,
);
let class_name_owned = decl
.name
.as_ref()
.and_then(|i| i.as_deref())
.unwrap_or("<anonymous>")
.to_string();
let class_name = class_name_owned.as_str();
let resolved = resolve_name(self.db, file.as_ref(), class_name);
let fqcn: &str = &resolved;
let here = crate::db::Fqcn::from_str(self.db, fqcn);
let parent_fqcn =
crate::db::find_class_like(self.db, here).and_then(|c| c.parent().cloned());
crate::attributes::check_parent_in_class_attrs(
&decl.attributes,
parent_fqcn.is_some(),
file,
source,
source_map,
all_issues,
);
if let Some(parent) = &decl.extends {
crate::diagnostics::check_name_class_for_extends(
parent,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
for iface in decl.implements.iter() {
check_name_class(
iface,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: parent_fqcn.clone(),
detect_ctor: true,
with_templates: true,
check_returns: true,
analyze_param_defaults: true,
};
for member in decl.body.members.iter() {
if let php_ast::owned::ClassMemberKind::Property(prop) = &member.kind {
self.check_property_member(
prop,
&member.span,
fqcn,
file,
source,
source_map,
all_issues,
);
continue;
}
let php_ast::owned::ClassMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
None,
);
}
self.check_trait_constraints(fqcn, file, all_issues);
}
#[allow(clippy::too_many_arguments)]
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);
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,
false,
true,
);
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_issues());
if let Some(fqn) = fqn {
self.record_function_inference(&fqn, &inferred);
}
}
#[allow(clippy::too_many_arguments)]
fn analyze_class_decl_typed(
&self,
decl: &php_ast::owned::ClassDecl,
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>,
) {
let class_name_owned = decl
.name
.as_ref()
.and_then(|i| i.as_deref())
.unwrap_or("<anonymous>")
.to_string();
let class_name = class_name_owned.as_str();
let resolved = resolve_name(self.db, file.as_ref(), class_name);
let fqcn: &str = &resolved;
let here = crate::db::Fqcn::from_str(self.db, fqcn);
let parent_fqcn =
crate::db::find_class_like(self.db, here).and_then(|c| c.parent().cloned());
if let Some(parent) = &decl.extends {
crate::diagnostics::check_name_class_for_extends(
parent,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
for iface in decl.implements.iter() {
check_name_class(
iface,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: parent_fqcn.clone(),
detect_ctor: true,
with_templates: false,
check_returns: false,
analyze_param_defaults: true,
};
for member in decl.body.members.iter() {
if let php_ast::owned::ClassMemberKind::Property(prop) = &member.kind {
self.check_property_member(
prop,
&member.span,
fqcn,
file,
source,
source_map,
all_issues,
);
continue;
}
let php_ast::owned::ClassMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
Some(&mut *type_envs),
);
}
self.check_trait_constraints(fqcn, file, all_issues);
}
fn check_trait_constraints(&self, fqcn: &str, file: &Arc<str>, all_issues: &mut Vec<Issue>) {
let here = crate::db::Fqcn::from_str(self.db, fqcn);
let Some(class) = crate::db::find_class_like(self.db, here) else {
return;
};
let trait_list: Vec<Arc<str>> = class.class_traits().to_vec();
let trait_locs: Vec<(Arc<str>, mir_types::Location)> = class.trait_use_locations().to_vec();
let class_all_parents: Vec<Arc<str>> = crate::db::class_ancestors(self.db, here).0;
for trait_fqcn in trait_list.iter() {
let tr_short: Arc<str> = trait_fqcn
.rsplit('\\')
.next()
.map(Arc::from)
.unwrap_or_else(|| trait_fqcn.clone());
let make_loc = || {
trait_locs
.iter()
.find(|(f, _)| f.as_ref() == trait_fqcn.as_ref())
.map(|(_, loc)| mir_issues::Location {
file: loc.file.clone(),
line: loc.line,
line_end: loc.line_end,
col_start: loc.col_start,
col_end: loc.col_end,
})
.unwrap_or_else(|| mir_issues::Location {
file: file.clone(),
line: 1,
line_end: 1,
col_start: 0,
col_end: 0,
})
};
let trait_here = crate::db::Fqcn::from_str(self.db, trait_fqcn.as_ref());
let trait_class = match crate::db::find_class_like(self.db, trait_here) {
None => {
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::UndefinedTrait {
name: tr_short.to_string(),
},
make_loc(),
));
continue;
}
Some(c) => c,
};
if !trait_class.is_trait() {
let (article, kind) = if trait_class.is_interface() {
("an", "interface")
} else if trait_class.is_enum() {
("an", "enum")
} else {
("a", "class")
};
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::InvalidTraitUse {
trait_name: tr_short.to_string(),
reason: format!("{tr_short} is {article} {kind}, not a trait"),
},
make_loc(),
));
continue;
}
let (req_ext, req_impl): (Vec<Arc<str>>, Vec<Arc<str>>) = match &trait_class {
crate::db::ClassLike::Trait(t) => {
(t.require_extends.to_vec(), t.require_implements.to_vec())
}
_ => (vec![], vec![]),
};
if req_ext.is_empty() && req_impl.is_empty() {
continue;
}
for req in req_ext.iter() {
let satisfies = fqcn == req.as_ref()
|| class_all_parents.iter().any(|p| p.as_ref() == req.as_ref());
if !satisfies {
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::InvalidTraitUse {
trait_name: tr_short.to_string(),
reason: format!(
"Class {fqcn} uses trait {tr_short} but does not extend {req}"
),
},
make_loc(),
));
}
}
for req in req_impl.iter() {
let satisfies = class_all_parents.iter().any(|p| p.as_ref() == req.as_ref());
if !satisfies {
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::InvalidTraitUse {
trait_name: tr_short.to_string(),
reason: format!(
"Class {fqcn} uses trait {tr_short} but does not implement {req}"
),
},
make_loc(),
));
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn analyze_trait_decl(
&self,
decl: &php_ast::owned::TraitDecl,
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_trait_attributes(
decl, self.db, file, source, source_map, all_issues,
);
let resolved = resolve_name(self.db, file.as_ref(), decl.name.as_deref().unwrap_or(""));
let fqcn: &str = &resolved;
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: None,
detect_ctor: true,
with_templates: false,
check_returns: false,
analyze_param_defaults: false,
};
for member in decl.body.members.iter() {
if let php_ast::owned::ClassMemberKind::Property(prop) = &member.kind {
self.check_property_member(
prop,
&member.span,
fqcn,
file,
source,
source_map,
all_issues,
);
continue;
}
let php_ast::owned::ClassMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
None,
);
}
}
#[allow(clippy::too_many_arguments)]
fn analyze_trait_decl_typed(
&self,
decl: &php_ast::owned::TraitDecl,
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>,
) {
let resolved = resolve_name(self.db, file.as_ref(), decl.name.as_deref().unwrap_or(""));
let fqcn: &str = &resolved;
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: None,
detect_ctor: true,
with_templates: false,
check_returns: false,
analyze_param_defaults: false,
};
for member in decl.body.members.iter() {
if let php_ast::owned::ClassMemberKind::Property(prop) = &member.kind {
self.check_property_member(
prop,
&member.span,
fqcn,
file,
source,
source_map,
all_issues,
);
continue;
}
let php_ast::owned::ClassMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
Some(&mut *type_envs),
);
}
}
pub(crate) fn analyze_enum_decl(
&self,
decl: &php_ast::owned::EnumDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use php_ast::owned::EnumMemberKind;
for iface in decl.implements.iter() {
check_name_class(
iface,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
let enum_name = decl.name.as_deref().unwrap_or("<anonymous>");
let resolved = resolve_name(self.db, file.as_ref(), enum_name);
let fqcn: &str = &resolved;
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: None,
detect_ctor: false,
with_templates: false,
check_returns: false,
analyze_param_defaults: false,
};
for member in decl.body.members.iter() {
let EnumMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
None,
);
}
}
#[allow(clippy::too_many_arguments)]
fn analyze_enum_decl_typed(
&self,
decl: &php_ast::owned::EnumDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
type_envs: &mut rustc_hash::FxHashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
all_symbols: &mut Vec<ResolvedSymbol>,
) {
use php_ast::owned::EnumMemberKind;
for iface in decl.implements.iter() {
check_name_class(
iface,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
let enum_name = decl.name.as_deref().unwrap_or("<anonymous>");
let resolved = resolve_name(self.db, file.as_ref(), enum_name);
let fqcn: &str = &resolved;
let scope_cx = MethodScopeCx {
fqcn: Arc::from(fqcn),
parent_fqcn: None,
detect_ctor: false,
with_templates: false,
check_returns: false,
analyze_param_defaults: false,
};
for member in decl.body.members.iter() {
let EnumMemberKind::Method(method) = &member.kind else {
continue;
};
self.analyze_method_scope(
method,
&scope_cx,
file,
source,
source_map,
all_issues,
all_symbols,
Some(&mut *type_envs),
);
}
}
pub(crate) fn analyze_interface_decl(
&self,
decl: &php_ast::owned::InterfaceDecl,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
) {
crate::attributes::check_interface_attributes(
decl, self.db, file, source, source_map, all_issues,
);
use php_ast::owned::ClassMemberKind;
for parent in decl.extends.iter() {
check_name_class(
parent,
self.db,
file,
source,
source_map,
all_issues,
self.php_version,
);
}
let iface_name = decl.name.as_deref().unwrap_or("<anonymous>");
let iface_fqcn = resolve_name(self.db, file.as_ref(), iface_name);
let iface_fqcn_ref = crate::db::Fqcn::from_str(self.db, &iface_fqcn);
for member in decl.body.members.iter() {
let ClassMemberKind::Method(method) = &member.kind else {
continue;
};
for param in method.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) = &method.return_type {
self.check_and_record_type_hint_classes(hint, file, source, source_map, all_issues);
}
let method_name = method.name.as_deref().unwrap_or("");
let stored = crate::db::find_method_in_class(self.db, iface_fqcn_ref, method_name);
if self.mode == AnalysisMode::Full {
let stored_return = stored.as_ref().and_then(|m| m.return_type.as_deref());
if method.return_type.is_none() && stored_return.is_none() {
let fn_name = format!("{iface_fqcn}::{method_name}");
let (line, col_start) = crate::diagnostics::offset_to_line_col(
source,
member.span.start,
source_map,
);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, member.span.end, source_map);
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MissingReturnType { fn_name },
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
if let Some(stored_method) = &stored {
let fn_name = format!("{iface_fqcn}::{method_name}");
for (ast_param, stored_param) in
method.params.iter().zip(stored_method.params.iter())
{
if ast_param.type_hint.is_none() && stored_param.ty.is_none() {
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,
);
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::MissingParamType {
fn_name: fn_name.clone(),
param: param_name,
},
mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
},
));
}
}
}
}
}
}
}
fn seed_param_locations(
ctx: &mut crate::flow_state::FlowState,
ast_params: &[php_ast::owned::Param],
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
) {
for p in ast_params.iter() {
let name_str = p.name.as_deref().unwrap_or("");
let name = name_str.trim_start_matches('$');
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, p.span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, p.span.end, source_map);
ctx.record_var_location(name, line, col_start, line_end, col_end);
if p.visibility.is_some() {
ctx.read_vars.insert(Name::from(name));
}
}
}
fn param_name_span(source: &str, p: &php_ast::owned::Param) -> php_ast::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;
php_ast::Span {
start,
end: start + needle.len() as u32,
}
} else {
p.span
}
}
fn record_param_symbols(
all_symbols: &mut Vec<ResolvedSymbol>,
file: &Arc<str>,
source: &str,
ast_params: &[php_ast::owned::Param],
ctx: &crate::flow_state::FlowState,
) {
use crate::symbol::ReferenceKind;
for p in ast_params {
let Some(raw) = p.name.as_deref() else {
continue;
};
let bare = raw.trim_start_matches('$');
let span = param_name_span(source, p);
let ty = ctx.get_var(bare);
all_symbols.push(ResolvedSymbol {
file: file.clone(),
span,
expr_span: None,
kind: ReferenceKind::Variable(Arc::from(bare)),
resolved_type: ty,
});
}
}
pub(crate) fn check_use_decl_casing(
use_decl: &php_ast::owned::UseDecl,
db: &dyn crate::db::MirDatabase,
file: &std::sync::Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
all_issues: &mut Vec<Issue>,
) {
use php_ast::ast::UseKind;
for item in use_decl.uses.iter() {
let effective_kind = item.kind.unwrap_or(use_decl.kind);
let full_name = crate::parser::name_to_string_owned(&item.name)
.trim_start_matches('\\')
.to_string();
if full_name.is_empty() {
continue;
}
let (line, col_start) =
crate::diagnostics::offset_to_line_col(source, item.span.start, source_map);
let (line_end, col_end) =
crate::diagnostics::offset_to_line_col(source, item.span.end, source_map);
let loc = mir_issues::Location {
file: file.clone(),
line,
line_end,
col_start,
col_end: col_end.max(col_start + 1),
};
match effective_kind {
UseKind::Normal => {
let here = crate::db::Fqcn::from_str(db, &full_name);
if let Some(class) = crate::db::find_class_like(db, here) {
let written_short = full_name.rsplit('\\').next().unwrap_or(full_name.as_str());
let canonical_short = class
.fqcn()
.rsplit('\\')
.next()
.unwrap_or(class.fqcn().as_ref());
if written_short != canonical_short
&& written_short.eq_ignore_ascii_case(canonical_short)
{
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::WrongCaseClass {
used: written_short.to_string(),
canonical: canonical_short.to_string(),
},
loc,
));
}
}
}
UseKind::Function => {
let here = crate::db::Fqcn::from_str(db, &full_name);
if let Some(func) = crate::db::find_function(db, here) {
let written_short = full_name.rsplit('\\').next().unwrap_or(full_name.as_str());
let canonical_short = func.fqn.rsplit('\\').next().unwrap_or(func.fqn.as_ref());
if written_short != canonical_short
&& written_short.eq_ignore_ascii_case(canonical_short)
{
all_issues.push(mir_issues::Issue::new(
mir_issues::IssueKind::WrongCaseFunction {
used: written_short.to_string(),
canonical: canonical_short.to_string(),
},
loc,
));
}
}
}
UseKind::Const => {}
}
}
}
pub(crate) fn body_has_yield(stmts: &[php_ast::owned::Stmt]) -> bool {
use php_ast::owned::visitor::{walk_owned_expr, walk_owned_stmt, OwnedVisitor};
use php_ast::owned::{ExprKind, StmtKind};
use std::ops::ControlFlow;
struct YieldFinder;
impl OwnedVisitor for YieldFinder {
fn visit_stmt(&mut self, stmt: &php_ast::owned::Stmt) -> ControlFlow<()> {
match &stmt.kind {
StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_)
| StmtKind::Enum(_) => ControlFlow::Continue(()),
_ => walk_owned_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &php_ast::owned::Expr) -> ControlFlow<()> {
match &expr.kind {
ExprKind::Yield(_) => ControlFlow::Break(()),
ExprKind::Closure(_) | ExprKind::ArrowFunction(_) => ControlFlow::Continue(()),
_ => walk_owned_expr(self, expr),
}
}
}
let mut finder = YieldFinder;
for stmt in stmts {
if finder.visit_stmt(stmt).is_break() {
return true;
}
}
false
}
pub fn merge_return_types(return_types: &[Type]) -> Type {
if return_types.is_empty() {
return Type::single(mir_types::Atomic::TVoid);
}
return_types.iter().fold(Type::empty(), |mut acc, t| {
acc.merge_with(t);
acc
})
}