1mod control_flow;
4mod declarations;
5mod expressions;
6mod flow;
7mod loops;
8mod return_type;
9
10use loops::{vars_stabilized, widen_unstable};
11pub(crate) use return_type::named_object_return_compatible;
12use return_type::resolve_union_for_file;
13
14use std::sync::Arc;
15
16use php_ast::ast::StmtKind;
17
18use mir_issues::{Issue, IssueBuffer, IssueKind, Location};
19use mir_types::Union;
20
21use crate::context::Context;
22use crate::db::MirDatabase;
23use crate::expr::ExpressionAnalyzer;
24use crate::php_version::PhpVersion;
25use crate::symbol::ResolvedSymbol;
26
27pub struct StatementsAnalyzer<'a> {
32 pub db: &'a dyn MirDatabase,
33 pub file: Arc<str>,
34 pub source: &'a str,
35 pub source_map: &'a php_rs_parser::source_map::SourceMap,
36 pub issues: &'a mut IssueBuffer,
37 pub symbols: &'a mut Vec<ResolvedSymbol>,
38 pub php_version: PhpVersion,
39 pub inference_only: bool,
40 pub return_types: Vec<Union>,
42 break_ctx_stack: Vec<Vec<Context>>,
45}
46
47impl<'a> StatementsAnalyzer<'a> {
48 #[allow(clippy::too_many_arguments)]
49 pub fn new(
50 db: &'a dyn MirDatabase,
51 file: Arc<str>,
52 source: &'a str,
53 source_map: &'a php_rs_parser::source_map::SourceMap,
54 issues: &'a mut IssueBuffer,
55 symbols: &'a mut Vec<ResolvedSymbol>,
56 php_version: PhpVersion,
57 inference_only: bool,
58 ) -> Self {
59 Self {
60 db,
61 file,
62 source,
63 source_map,
64 issues,
65 symbols,
66 php_version,
67 inference_only,
68 return_types: Vec::new(),
69 break_ctx_stack: Vec::new(),
70 }
71 }
72
73 pub fn analyze_stmts<'arena, 'src>(
74 &mut self,
75 stmts: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Stmt<'arena, 'src>>,
76 ctx: &mut Context,
77 ) {
78 for stmt in stmts.iter() {
79 let suppressions = self.extract_statement_suppressions(stmt.span);
81 let before = self.issues.issue_count();
82
83 if ctx.diverges {
84 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
85 let (line_end, col_end) = if stmt.span.start < stmt.span.end {
86 let (end_line, end_col) = self.offset_to_line_col(stmt.span.end);
87 (end_line, end_col)
88 } else {
89 (line, col_start + 1)
90 };
91 self.issues.add(
92 Issue::new(
93 IssueKind::UnreachableCode,
94 Location {
95 file: self.file.clone(),
96 line,
97 line_end,
98 col_start,
99 col_end: col_end.max(col_start + 1),
100 },
101 )
102 .with_snippet(
103 crate::parser::span_text(self.source, stmt.span).unwrap_or_default(),
104 ),
105 );
106 if !suppressions.is_empty() {
107 self.issues.suppress_range(before, &suppressions);
108 }
109 break;
110 }
111
112 let var_annotation = self.extract_var_annotation(stmt.span);
114
115 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
118 ctx.set_var(var_name.as_str(), var_ty.clone());
119 }
120
121 self.analyze_stmt(stmt, ctx);
122
123 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
127 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
128 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
129 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
130 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
131 let lhs = lhs_name.trim_start_matches('$');
132 if lhs == var_name.as_str() {
133 ctx.set_var(var_name.as_str(), var_ty.clone());
134 }
135 }
136 }
137 }
138 }
139 }
140
141 if !suppressions.is_empty() {
142 self.issues.suppress_range(before, &suppressions);
143 }
144 }
145 }
146
147 pub fn analyze_stmt<'arena, 'src>(
148 &mut self,
149 stmt: &php_ast::ast::Stmt<'arena, 'src>,
150 ctx: &mut Context,
151 ) {
152 match &stmt.kind {
153 StmtKind::Expression(expr) => {
155 self.analyze_expression_stmt(expr, ctx);
156 }
157
158 StmtKind::Echo(exprs) => {
160 self.analyze_echo_stmt(exprs, stmt.span, ctx);
161 }
162
163 StmtKind::Return(opt_expr) => {
165 self.analyze_return_stmt(opt_expr, stmt.span, ctx);
166 }
167
168 StmtKind::Throw(expr) => {
170 self.analyze_throw_stmt(expr, stmt.span, ctx);
171 }
172
173 StmtKind::If(if_stmt) => {
175 self.analyze_if_stmt(if_stmt, ctx);
176 }
177
178 StmtKind::While(w) => {
180 self.analyze_while_stmt(w, ctx);
181 }
182
183 StmtKind::DoWhile(dw) => {
185 self.analyze_dowhile_stmt(dw, ctx);
186 }
187
188 StmtKind::For(f) => {
190 self.analyze_for_stmt(f, ctx);
191 }
192
193 StmtKind::Foreach(fe) => {
195 self.analyze_foreach_stmt(fe, stmt.span, ctx);
196 }
197
198 StmtKind::Switch(sw) => {
200 self.analyze_switch_stmt(sw, ctx);
201 }
202
203 StmtKind::TryCatch(tc) => {
205 self.analyze_trycatch_stmt(tc, ctx);
206 }
207
208 StmtKind::Block(stmts) => {
210 self.analyze_stmts(stmts, ctx);
211 }
212
213 StmtKind::Break(_) => {
215 self.analyze_break_stmt(ctx);
216 }
217
218 StmtKind::Continue(_) => {
220 self.analyze_continue_stmt(ctx);
221 }
222
223 StmtKind::Unset(vars) => {
225 self.analyze_unset_stmt(vars, ctx);
226 }
227
228 StmtKind::StaticVar(vars) => {
230 self.analyze_static_var_stmt(vars, ctx);
231 }
232
233 StmtKind::Global(vars) => {
235 self.analyze_global_stmt(vars, ctx);
236 }
237
238 StmtKind::Declare(d) => {
240 self.analyze_declare_stmt(d, ctx);
241 }
242
243 StmtKind::Function(decl) => {
245 self.analyze_function_decl_stmt(decl, ctx);
246 }
247
248 StmtKind::Class(decl) => {
249 self.analyze_class_decl_stmt(decl, ctx);
250 }
251
252 StmtKind::Interface(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {
253 }
255
256 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
258
259 StmtKind::InlineHtml(_)
261 | StmtKind::Nop
262 | StmtKind::Goto(_)
263 | StmtKind::Label(_)
264 | StmtKind::HaltCompiler(_) => {}
265
266 StmtKind::Error => {}
267 }
268 }
269
270 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
275 where
276 'a: 'b,
277 {
278 ExpressionAnalyzer::new(
279 self.db,
280 self.file.clone(),
281 self.source,
282 self.source_map,
283 self.issues,
284 self.symbols,
285 self.php_version,
286 self.inference_only,
287 )
288 }
289
290 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
293 let lc = self.source_map.offset_to_line_col(offset);
294 let line = lc.line + 1;
295
296 let byte_offset = offset as usize;
297 let line_start_byte = if byte_offset == 0 {
298 0
299 } else {
300 self.source[..byte_offset]
301 .rfind('\n')
302 .map(|p| p + 1)
303 .unwrap_or(0)
304 };
305
306 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
307
308 (line, col)
309 }
310
311 fn check_name_undefined_class(&mut self, name: &php_ast::ast::Name<'_, '_>) {
313 let raw = crate::parser::name_to_string(name);
314 let resolved = crate::db::resolve_name_via_db(self.db, &self.file, &raw);
315 if matches!(resolved.as_str(), "self" | "static" | "parent") {
316 return;
317 }
318 if crate::db::type_exists_via_db(self.db, &resolved) {
319 return;
320 }
321 let span = name.span();
322 let (line, col_start) = self.offset_to_line_col(span.start);
323 let (line_end, col_end) = self.offset_to_line_col(span.end);
324 self.issues.add(Issue::new(
325 IssueKind::UndefinedClass { name: resolved },
326 Location {
327 file: self.file.clone(),
328 line,
329 line_end,
330 col_start,
331 col_end: col_end.max(col_start + 1),
332 },
333 ));
334 }
335
336 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
343 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
344 return vec![];
345 };
346 let mut suppressions = Vec::new();
347 for line in doc.lines() {
348 let line = line.trim().trim_start_matches('*').trim();
349 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
350 r
351 } else if let Some(r) = line.strip_prefix("@suppress ") {
352 r
353 } else {
354 continue;
355 };
356 for name in rest.split_whitespace() {
357 suppressions.push(name.to_string());
358 }
359 }
360 suppressions
361 }
362
363 fn extract_var_annotation(
367 &self,
368 span: php_ast::Span,
369 ) -> Option<(Option<String>, mir_types::Union)> {
370 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
371 let parsed = crate::parser::DocblockParser::parse(&doc);
372 let ty = parsed.var_type?;
373 let resolved = resolve_union_for_file(ty, self.db, &self.file);
374 Some((parsed.var_name, resolved))
375 }
376
377 fn analyze_loop_widened<F>(
393 &mut self,
394 pre: &Context,
395 entry: Context,
396 mut body: F,
397 loop_guaranteed: bool,
398 ) -> Context
399 where
400 F: FnMut(&mut Self, &mut Context),
401 {
402 const MAX_ITERS: usize = 3;
403
404 self.break_ctx_stack.push(Vec::new());
406
407 let mut current = entry;
408 current.inside_loop = true;
409
410 for _ in 0..MAX_ITERS {
411 let prev_vars = current.vars.clone();
412
413 let mut iter = current.clone();
414 body(self, &mut iter);
415
416 let next = Context::merge_branches(pre, iter, None);
417
418 if vars_stabilized(&prev_vars, &next.vars) {
419 current = next;
420 break;
421 }
422 current = next;
423 }
424
425 widen_unstable(&pre.vars, &mut current.vars, loop_guaranteed);
427
428 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
430 for bctx in break_ctxs {
431 current = Context::merge_branches(pre, current, Some(bctx));
432 }
433
434 current
435 }
436}