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 var_annotation.is_none() {
145 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
146 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
147 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
148 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
149 let lhs = lhs_name.trim_start_matches('$').to_string();
150 if let Some(doc) = crate::parser::find_preceding_docblock(
152 self.source,
153 stmt.span.start,
154 ) {
155 let parsed = crate::parser::DocblockParser::parse(&doc);
156 if let Some(var_type) = parsed.var_type {
157 if let Some(var_name) = parsed.var_name {
159 if var_name == lhs {
160 let resolved = crate::stmt::return_type::resolve_union_for_file(var_type, self.db, &self.file);
161 ctx.set_var(&lhs, resolved);
162 }
163 }
164 }
165 }
166 }
167 }
168 }
169 }
170 }
171
172 if !suppressions.is_empty() {
173 self.issues.suppress_range(before, &suppressions);
174 }
175 }
176 }
177
178 pub fn analyze_stmt<'arena, 'src>(
179 &mut self,
180 stmt: &php_ast::ast::Stmt<'arena, 'src>,
181 ctx: &mut Context,
182 ) {
183 let var_annotation = self.extract_var_annotation(stmt.span);
185
186 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
188 ctx.set_var(var_name.as_str(), var_ty.clone());
189 }
190
191 match &stmt.kind {
192 StmtKind::Expression(expr) => {
194 self.analyze_expression_stmt(expr, ctx);
195 }
196
197 StmtKind::Echo(exprs) => {
199 self.analyze_echo_stmt(exprs, stmt.span, ctx);
200 }
201
202 StmtKind::Return(opt_expr) => {
204 self.analyze_return_stmt(opt_expr, stmt.span, ctx);
205 }
206
207 StmtKind::Throw(expr) => {
209 self.analyze_throw_stmt(expr, stmt.span, ctx);
210 }
211
212 StmtKind::If(if_stmt) => {
214 self.analyze_if_stmt(if_stmt, ctx);
215 }
216
217 StmtKind::While(w) => {
219 self.analyze_while_stmt(w, ctx);
220 }
221
222 StmtKind::DoWhile(dw) => {
224 self.analyze_dowhile_stmt(dw, ctx);
225 }
226
227 StmtKind::For(f) => {
229 self.analyze_for_stmt(f, ctx);
230 }
231
232 StmtKind::Foreach(fe) => {
234 self.analyze_foreach_stmt(fe, stmt.span, ctx);
235 }
236
237 StmtKind::Switch(sw) => {
239 self.analyze_switch_stmt(sw, ctx);
240 }
241
242 StmtKind::TryCatch(tc) => {
244 self.analyze_trycatch_stmt(tc, ctx);
245 }
246
247 StmtKind::Block(stmts) => {
249 self.analyze_stmts(stmts, ctx);
250 }
251
252 StmtKind::Break(_) => {
254 self.analyze_break_stmt(ctx);
255 }
256
257 StmtKind::Continue(_) => {
259 self.analyze_continue_stmt(ctx);
260 }
261
262 StmtKind::Unset(vars) => {
264 self.analyze_unset_stmt(vars, ctx);
265 }
266
267 StmtKind::StaticVar(vars) => {
269 self.analyze_static_var_stmt(vars, ctx);
270 }
271
272 StmtKind::Global(vars) => {
274 self.analyze_global_stmt(vars, ctx);
275 }
276
277 StmtKind::Declare(d) => {
279 self.analyze_declare_stmt(d, ctx);
280 }
281
282 StmtKind::Function(decl) => {
284 self.analyze_function_decl_stmt(decl, ctx);
285 }
286
287 StmtKind::Class(decl) => {
288 self.analyze_class_decl_stmt(decl, ctx);
289 }
290
291 StmtKind::Interface(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {
292 }
294
295 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
297
298 StmtKind::InlineHtml(_)
300 | StmtKind::Nop
301 | StmtKind::Goto(_)
302 | StmtKind::Label(_)
303 | StmtKind::HaltCompiler(_) => {}
304
305 StmtKind::Error => {}
306 }
307
308 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
312 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
313 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
314 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
315 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
316 let lhs = lhs_name.trim_start_matches('$');
317 if lhs == var_name.as_str() {
318 ctx.set_var(var_name.as_str(), var_ty.clone());
319 }
320 }
321 }
322 }
323 }
324 }
325
326 if var_annotation.is_none() {
329 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
330 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
331 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
332 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
333 let lhs = lhs_name.trim_start_matches('$').to_string();
334 if let Some(doc) =
335 crate::parser::find_preceding_docblock(self.source, stmt.span.start)
336 {
337 let parsed = crate::parser::DocblockParser::parse(&doc);
338 if let Some(var_type) = parsed.var_type {
339 if let Some(var_name) = parsed.var_name {
340 if var_name == lhs {
341 let resolved =
342 crate::stmt::return_type::resolve_union_for_file(
343 var_type, self.db, &self.file,
344 );
345 ctx.set_var(&lhs, resolved);
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354 }
355 }
356
357 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
362 where
363 'a: 'b,
364 {
365 ExpressionAnalyzer::new(
366 self.db,
367 self.file.clone(),
368 self.source,
369 self.source_map,
370 self.issues,
371 self.symbols,
372 self.php_version,
373 self.inference_only,
374 )
375 }
376
377 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
380 let lc = self.source_map.offset_to_line_col(offset);
381 let line = lc.line + 1;
382
383 let byte_offset = offset as usize;
384 let line_start_byte = if byte_offset == 0 {
385 0
386 } else {
387 self.source[..byte_offset]
388 .rfind('\n')
389 .map(|p| p + 1)
390 .unwrap_or(0)
391 };
392
393 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
394
395 (line, col)
396 }
397
398 fn span_to_location(&self, span: php_ast::Span) -> (u32, u32, u16, u16) {
400 let (line, col_start) = self.offset_to_line_col(span.start);
401 let (line_end, col_end) = if span.start < span.end {
402 self.offset_to_line_col(span.end)
403 } else {
404 (line, col_start)
405 };
406 (line, line_end, col_start, col_end)
407 }
408
409 fn check_name_undefined_class(&mut self, name: &php_ast::ast::Name<'_, '_>) {
411 let raw = crate::parser::name_to_string(name);
412 let resolved = crate::db::resolve_name_via_db(self.db, &self.file, &raw);
413 if matches!(resolved.as_str(), "self" | "static" | "parent") {
414 return;
415 }
416 if crate::db::type_exists_via_db(self.db, &resolved) {
417 return;
418 }
419 let span = name.span();
420 let (line, col_start) = self.offset_to_line_col(span.start);
421 let (line_end, col_end) = self.offset_to_line_col(span.end);
422 self.issues.add(Issue::new(
423 IssueKind::UndefinedClass { name: resolved },
424 Location {
425 file: self.file.clone(),
426 line,
427 line_end,
428 col_start,
429 col_end: col_end.max(col_start + 1),
430 },
431 ));
432 }
433
434 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
441 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
442 return vec![];
443 };
444 let mut suppressions = Vec::new();
445 for line in doc.lines() {
446 let line = line.trim().trim_start_matches('*').trim();
447 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
448 r
449 } else if let Some(r) = line.strip_prefix("@suppress ") {
450 r
451 } else {
452 continue;
453 };
454 for name in rest.split_whitespace() {
455 suppressions.push(name.to_string());
456 }
457 }
458 suppressions
459 }
460
461 fn extract_var_annotation(
465 &self,
466 span: php_ast::Span,
467 ) -> Option<(Option<String>, mir_types::Union)> {
468 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
469 let parsed = crate::parser::DocblockParser::parse(&doc);
470 let ty = parsed.var_type?;
471 let resolved = resolve_union_for_file(ty, self.db, &self.file);
472 Some((parsed.var_name, resolved))
473 }
474
475 fn analyze_loop_widened<F>(
491 &mut self,
492 pre: &Context,
493 entry: Context,
494 mut body: F,
495 loop_guaranteed: bool,
496 ) -> Context
497 where
498 F: FnMut(&mut Self, &mut Context),
499 {
500 const MAX_ITERS: usize = 3;
501
502 self.break_ctx_stack.push(Vec::new());
504
505 let mut current = entry;
506 current.inside_loop = true;
507
508 for _ in 0..MAX_ITERS {
509 let prev_vars = current.vars.clone();
510
511 let mut iter = current.clone();
512 body(self, &mut iter);
513
514 let next = Context::merge_branches(pre, iter, None);
515
516 if vars_stabilized(&prev_vars, &next.vars) {
517 current = next;
518 break;
519 }
520 current = next;
521 }
522
523 widen_unstable(&pre.vars, &mut current.vars, loop_guaranteed);
525
526 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
528 for bctx in break_ctxs {
529 current = Context::merge_branches(pre, current, Some(bctx));
530 }
531
532 current
533 }
534}