1use std::sync::Arc;
4
5use php_ast::ast::StmtKind;
6
7use mir_codebase::Codebase;
8use mir_issues::{Issue, IssueBuffer, IssueKind, Location};
9use mir_types::{ArrayKey, Atomic, Union};
10
11use crate::context::Context;
12use crate::expr::ExpressionAnalyzer;
13use crate::narrowing::narrow_from_condition;
14use crate::php_version::PhpVersion;
15use crate::symbol::ResolvedSymbol;
16
17pub struct StatementsAnalyzer<'a> {
22 pub codebase: &'a Codebase,
23 pub file: Arc<str>,
24 pub source: &'a str,
25 pub source_map: &'a php_rs_parser::source_map::SourceMap,
26 pub issues: &'a mut IssueBuffer,
27 pub symbols: &'a mut Vec<ResolvedSymbol>,
28 pub php_version: PhpVersion,
29 pub return_types: Vec<Union>,
31 break_ctx_stack: Vec<Vec<Context>>,
34}
35
36impl<'a> StatementsAnalyzer<'a> {
37 pub fn new(
38 codebase: &'a Codebase,
39 file: Arc<str>,
40 source: &'a str,
41 source_map: &'a php_rs_parser::source_map::SourceMap,
42 issues: &'a mut IssueBuffer,
43 symbols: &'a mut Vec<ResolvedSymbol>,
44 php_version: PhpVersion,
45 ) -> Self {
46 Self {
47 codebase,
48 file,
49 source,
50 source_map,
51 issues,
52 symbols,
53 php_version,
54 return_types: Vec::new(),
55 break_ctx_stack: Vec::new(),
56 }
57 }
58
59 pub fn analyze_stmts<'arena, 'src>(
60 &mut self,
61 stmts: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Stmt<'arena, 'src>>,
62 ctx: &mut Context,
63 ) {
64 for stmt in stmts.iter() {
65 let suppressions = self.extract_statement_suppressions(stmt.span);
67 let before = self.issues.issue_count();
68
69 let var_annotation = self.extract_var_annotation(stmt.span);
71
72 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
75 ctx.set_var(var_name.as_str(), var_ty.clone());
76 }
77
78 self.analyze_stmt(stmt, ctx);
79
80 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
84 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
85 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
86 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
87 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
88 let lhs = lhs_name.trim_start_matches('$');
89 if lhs == var_name.as_str() {
90 ctx.set_var(var_name.as_str(), var_ty.clone());
91 }
92 }
93 }
94 }
95 }
96 }
97
98 if !suppressions.is_empty() {
99 self.issues.suppress_range(before, &suppressions);
100 }
101 }
102 }
103
104 pub fn analyze_stmt<'arena, 'src>(
105 &mut self,
106 stmt: &php_ast::ast::Stmt<'arena, 'src>,
107 ctx: &mut Context,
108 ) {
109 match &stmt.kind {
110 StmtKind::Expression(expr) => {
112 self.expr_analyzer(ctx).analyze(expr, ctx);
113 if let php_ast::ast::ExprKind::FunctionCall(call) = &expr.kind {
115 if let php_ast::ast::ExprKind::Identifier(fn_name) = &call.name.kind {
116 if fn_name.eq_ignore_ascii_case("assert") {
117 if let Some(arg) = call.args.first() {
118 narrow_from_condition(
119 &arg.value,
120 ctx,
121 true,
122 self.codebase,
123 &self.file,
124 );
125 }
126 }
127 }
128 }
129 }
130
131 StmtKind::Echo(exprs) => {
133 for expr in exprs.iter() {
134 if crate::taint::is_expr_tainted(expr, ctx) {
136 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
137 let col_end = if stmt.span.start < stmt.span.end {
138 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
139 end_col
140 } else {
141 col_start
142 };
143 let mut issue = mir_issues::Issue::new(
144 IssueKind::TaintedHtml,
145 mir_issues::Location {
146 file: self.file.clone(),
147 line,
148 col_start,
149 col_end: col_end.max(col_start + 1),
150 },
151 );
152 let start = stmt.span.start as usize;
154 let end = stmt.span.end as usize;
155 if start < self.source.len() {
156 let end = end.min(self.source.len());
157 let span_text = &self.source[start..end];
158 if let Some(first_line) = span_text.lines().next() {
159 issue = issue.with_snippet(first_line.trim().to_string());
160 }
161 }
162 self.issues.add(issue);
163 }
164 self.expr_analyzer(ctx).analyze(expr, ctx);
165 }
166 }
167
168 StmtKind::Return(opt_expr) => {
170 if let Some(expr) = opt_expr {
171 let ret_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
172
173 let check_ty =
178 if let Some((None, var_ty)) = self.extract_var_annotation(stmt.span) {
179 var_ty
180 } else {
181 ret_ty.clone()
182 };
183
184 if let Some(declared) = &ctx.fn_return_type.clone() {
186 if (declared.is_void() && !check_ty.is_void() && !check_ty.is_mixed())
190 || (!check_ty.is_subtype_of_simple(declared)
191 && !declared.is_mixed()
192 && !check_ty.is_mixed()
193 && !named_object_return_compatible(&check_ty, declared, self.codebase, &self.file)
194 && (check_ty.remove_null().is_empty() || !named_object_return_compatible(&check_ty.remove_null(), declared, self.codebase, &self.file))
198 && !declared_return_has_template(declared, self.codebase)
199 && !declared_return_has_template(&check_ty, self.codebase)
200 && !return_arrays_compatible(&check_ty, declared, self.codebase, &self.file)
201 && !declared.is_subtype_of_simple(&check_ty)
203 && !declared.remove_null().is_subtype_of_simple(&check_ty)
204 && (check_ty.remove_null().is_empty() || !check_ty.remove_null().is_subtype_of_simple(declared))
209 && !check_ty.remove_false().is_subtype_of_simple(declared)
210 && !named_object_return_compatible(declared, &check_ty, self.codebase, &self.file)
213 && !named_object_return_compatible(&declared.remove_null(), &check_ty.remove_null(), self.codebase, &self.file))
214 {
215 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
216 let col_end = if stmt.span.start < stmt.span.end {
217 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
218 end_col
219 } else {
220 col_start
221 };
222 self.issues.add(
223 mir_issues::Issue::new(
224 IssueKind::InvalidReturnType {
225 expected: format!("{}", declared),
226 actual: format!("{}", ret_ty),
227 },
228 mir_issues::Location {
229 file: self.file.clone(),
230 line,
231 col_start,
232 col_end: col_end.max(col_start + 1),
233 },
234 )
235 .with_snippet(
236 crate::parser::span_text(self.source, stmt.span)
237 .unwrap_or_default(),
238 ),
239 );
240 }
241 }
242 self.return_types.push(ret_ty);
243 } else {
244 self.return_types.push(Union::single(Atomic::TVoid));
245 if let Some(declared) = &ctx.fn_return_type.clone() {
247 if !declared.is_void() && !declared.is_mixed() {
248 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
249 let col_end = if stmt.span.start < stmt.span.end {
250 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
251 end_col
252 } else {
253 col_start
254 };
255 self.issues.add(
256 mir_issues::Issue::new(
257 IssueKind::InvalidReturnType {
258 expected: format!("{}", declared),
259 actual: "void".to_string(),
260 },
261 mir_issues::Location {
262 file: self.file.clone(),
263 line,
264 col_start,
265 col_end: col_end.max(col_start + 1),
266 },
267 )
268 .with_snippet(
269 crate::parser::span_text(self.source, stmt.span)
270 .unwrap_or_default(),
271 ),
272 );
273 }
274 }
275 }
276 ctx.diverges = true;
277 }
278
279 StmtKind::Throw(expr) => {
281 let thrown_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
282 for atomic in &thrown_ty.types {
284 match atomic {
285 mir_types::Atomic::TNamedObject { fqcn, .. } => {
286 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
287 let is_throwable = resolved == "Throwable"
288 || resolved == "Exception"
289 || resolved == "Error"
290 || fqcn.as_ref() == "Throwable"
291 || fqcn.as_ref() == "Exception"
292 || fqcn.as_ref() == "Error"
293 || self.codebase.extends_or_implements(&resolved, "Throwable")
294 || self.codebase.extends_or_implements(&resolved, "Exception")
295 || self.codebase.extends_or_implements(&resolved, "Error")
296 || self.codebase.extends_or_implements(fqcn, "Throwable")
297 || self.codebase.extends_or_implements(fqcn, "Exception")
298 || self.codebase.extends_or_implements(fqcn, "Error")
299 || self.codebase.has_unknown_ancestor(&resolved)
301 || self.codebase.has_unknown_ancestor(fqcn)
302 || (!self.codebase.type_exists(&resolved) && !self.codebase.type_exists(fqcn));
304 if !is_throwable {
305 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
306 let col_end = if stmt.span.start < stmt.span.end {
307 let (_end_line, end_col) =
308 self.offset_to_line_col(stmt.span.end);
309 end_col
310 } else {
311 col_start
312 };
313 self.issues.add(mir_issues::Issue::new(
314 IssueKind::InvalidThrow {
315 ty: fqcn.to_string(),
316 },
317 mir_issues::Location {
318 file: self.file.clone(),
319 line,
320 col_start,
321 col_end: col_end.max(col_start + 1),
322 },
323 ));
324 }
325 }
326 mir_types::Atomic::TSelf { fqcn }
328 | mir_types::Atomic::TStaticObject { fqcn }
329 | mir_types::Atomic::TParent { fqcn } => {
330 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
331 let is_throwable = resolved == "Throwable"
332 || resolved == "Exception"
333 || resolved == "Error"
334 || self.codebase.extends_or_implements(&resolved, "Throwable")
335 || self.codebase.extends_or_implements(&resolved, "Exception")
336 || self.codebase.extends_or_implements(&resolved, "Error")
337 || self.codebase.extends_or_implements(fqcn, "Throwable")
338 || self.codebase.extends_or_implements(fqcn, "Exception")
339 || self.codebase.extends_or_implements(fqcn, "Error")
340 || self.codebase.has_unknown_ancestor(&resolved)
341 || self.codebase.has_unknown_ancestor(fqcn);
342 if !is_throwable {
343 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
344 let col_end = if stmt.span.start < stmt.span.end {
345 let (_end_line, end_col) =
346 self.offset_to_line_col(stmt.span.end);
347 end_col
348 } else {
349 col_start
350 };
351 self.issues.add(mir_issues::Issue::new(
352 IssueKind::InvalidThrow {
353 ty: fqcn.to_string(),
354 },
355 mir_issues::Location {
356 file: self.file.clone(),
357 line,
358 col_start,
359 col_end: col_end.max(col_start + 1),
360 },
361 ));
362 }
363 }
364 mir_types::Atomic::TMixed | mir_types::Atomic::TObject => {}
365 _ => {
366 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
367 let col_end = if stmt.span.start < stmt.span.end {
368 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
369 end_col
370 } else {
371 col_start
372 };
373 self.issues.add(mir_issues::Issue::new(
374 IssueKind::InvalidThrow {
375 ty: format!("{}", thrown_ty),
376 },
377 mir_issues::Location {
378 file: self.file.clone(),
379 line,
380 col_start,
381 col_end: col_end.max(col_start + 1),
382 },
383 ));
384 }
385 }
386 }
387 ctx.diverges = true;
388 }
389
390 StmtKind::If(if_stmt) => {
392 let pre_ctx = ctx.clone();
393
394 let cond_type = self.expr_analyzer(ctx).analyze(&if_stmt.condition, ctx);
396 let pre_diverges = ctx.diverges;
397
398 let mut then_ctx = ctx.fork();
400 narrow_from_condition(
401 &if_stmt.condition,
402 &mut then_ctx,
403 true,
404 self.codebase,
405 &self.file,
406 );
407 let then_unreachable_from_narrowing = then_ctx.diverges;
411 if !then_ctx.diverges {
414 self.analyze_stmt(if_stmt.then_branch, &mut then_ctx);
415 }
416
417 let mut elseif_ctxs: Vec<Context> = vec![];
419 for elseif in if_stmt.elseif_branches.iter() {
420 let mut pre_elseif = ctx.fork();
423 narrow_from_condition(
424 &if_stmt.condition,
425 &mut pre_elseif,
426 false,
427 self.codebase,
428 &self.file,
429 );
430 let pre_elseif_diverges = pre_elseif.diverges;
431
432 let mut elseif_true_ctx = pre_elseif.clone();
436 narrow_from_condition(
437 &elseif.condition,
438 &mut elseif_true_ctx,
439 true,
440 self.codebase,
441 &self.file,
442 );
443 let mut elseif_false_ctx = pre_elseif.clone();
444 narrow_from_condition(
445 &elseif.condition,
446 &mut elseif_false_ctx,
447 false,
448 self.codebase,
449 &self.file,
450 );
451 if !pre_elseif_diverges
452 && (elseif_true_ctx.diverges || elseif_false_ctx.diverges)
453 {
454 let (line, col_start) =
455 self.offset_to_line_col(elseif.condition.span.start);
456 let col_end = if elseif.condition.span.start < elseif.condition.span.end {
457 let (_end_line, end_col) =
458 self.offset_to_line_col(elseif.condition.span.end);
459 end_col
460 } else {
461 col_start
462 };
463 let elseif_cond_type = self
464 .expr_analyzer(ctx)
465 .analyze(&elseif.condition, &mut ctx.fork());
466 self.issues.add(
467 mir_issues::Issue::new(
468 IssueKind::RedundantCondition {
469 ty: format!("{}", elseif_cond_type),
470 },
471 mir_issues::Location {
472 file: self.file.clone(),
473 line,
474 col_start,
475 col_end: col_end.max(col_start + 1),
476 },
477 )
478 .with_snippet(
479 crate::parser::span_text(self.source, elseif.condition.span)
480 .unwrap_or_default(),
481 ),
482 );
483 }
484
485 let mut branch_ctx = elseif_true_ctx;
487 self.expr_analyzer(&branch_ctx)
488 .analyze(&elseif.condition, &mut branch_ctx);
489 if !branch_ctx.diverges {
490 self.analyze_stmt(&elseif.body, &mut branch_ctx);
491 }
492 elseif_ctxs.push(branch_ctx);
493 }
494
495 let mut else_ctx = ctx.fork();
497 narrow_from_condition(
498 &if_stmt.condition,
499 &mut else_ctx,
500 false,
501 self.codebase,
502 &self.file,
503 );
504 let else_unreachable_from_narrowing = else_ctx.diverges;
505 if !else_ctx.diverges {
506 if let Some(else_branch) = &if_stmt.else_branch {
507 self.analyze_stmt(else_branch, &mut else_ctx);
508 }
509 }
510
511 if !pre_diverges
513 && (then_unreachable_from_narrowing || else_unreachable_from_narrowing)
514 {
515 let (line, col_start) = self.offset_to_line_col(if_stmt.condition.span.start);
516 let col_end = if if_stmt.condition.span.start < if_stmt.condition.span.end {
517 let (_end_line, end_col) =
518 self.offset_to_line_col(if_stmt.condition.span.end);
519 end_col
520 } else {
521 col_start
522 };
523 self.issues.add(
524 mir_issues::Issue::new(
525 IssueKind::RedundantCondition {
526 ty: format!("{}", cond_type),
527 },
528 mir_issues::Location {
529 file: self.file.clone(),
530 line,
531 col_start,
532 col_end: col_end.max(col_start + 1),
533 },
534 )
535 .with_snippet(
536 crate::parser::span_text(self.source, if_stmt.condition.span)
537 .unwrap_or_default(),
538 ),
539 );
540 }
541
542 *ctx = Context::merge_branches(&pre_ctx, then_ctx, Some(else_ctx));
547 for ec in elseif_ctxs {
548 *ctx = Context::merge_branches(&pre_ctx, ec, Some(ctx.clone()));
549 }
550 }
551
552 StmtKind::While(w) => {
554 self.expr_analyzer(ctx).analyze(&w.condition, ctx);
555 let pre = ctx.clone();
556
557 let mut entry = ctx.fork();
559 narrow_from_condition(&w.condition, &mut entry, true, self.codebase, &self.file);
560
561 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
562 sa.analyze_stmt(w.body, iter);
563 sa.expr_analyzer(iter).analyze(&w.condition, iter);
564 });
565 *ctx = post;
566 }
567
568 StmtKind::DoWhile(dw) => {
570 let pre = ctx.clone();
571 let entry = ctx.fork();
572 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
573 sa.analyze_stmt(dw.body, iter);
574 sa.expr_analyzer(iter).analyze(&dw.condition, iter);
575 });
576 *ctx = post;
577 }
578
579 StmtKind::For(f) => {
581 for init in f.init.iter() {
583 self.expr_analyzer(ctx).analyze(init, ctx);
584 }
585 let pre = ctx.clone();
586 let mut entry = ctx.fork();
587 for cond in f.condition.iter() {
588 self.expr_analyzer(&entry).analyze(cond, &mut entry);
589 }
590
591 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
592 sa.analyze_stmt(f.body, iter);
593 for update in f.update.iter() {
594 sa.expr_analyzer(iter).analyze(update, iter);
595 }
596 for cond in f.condition.iter() {
597 sa.expr_analyzer(iter).analyze(cond, iter);
598 }
599 });
600 *ctx = post;
601 }
602
603 StmtKind::Foreach(fe) => {
605 let arr_ty = self.expr_analyzer(ctx).analyze(&fe.expr, ctx);
606 let (key_ty, mut value_ty) = infer_foreach_types(&arr_ty);
607
608 if let Some(vname) = crate::expr::extract_simple_var(&fe.value) {
611 if let Some((Some(ann_var), ann_ty)) = self.extract_var_annotation(stmt.span) {
612 if ann_var == vname {
613 value_ty = ann_ty;
614 }
615 }
616 }
617
618 let pre = ctx.clone();
619 let mut entry = ctx.fork();
620
621 if let Some(key_expr) = &fe.key {
623 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
624 entry.set_var(var_name, key_ty.clone());
625 }
626 }
627 let value_var = crate::expr::extract_simple_var(&fe.value);
630 let value_destructure_vars = crate::expr::extract_destructure_vars(&fe.value);
631 if let Some(ref vname) = value_var {
632 entry.set_var(vname.as_str(), value_ty.clone());
633 } else {
634 for vname in &value_destructure_vars {
635 entry.set_var(vname, Union::mixed());
636 }
637 }
638
639 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
640 if let Some(key_expr) = &fe.key {
642 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
643 iter.set_var(var_name, key_ty.clone());
644 }
645 }
646 if let Some(ref vname) = value_var {
647 iter.set_var(vname.as_str(), value_ty.clone());
648 } else {
649 for vname in &value_destructure_vars {
650 iter.set_var(vname, Union::mixed());
651 }
652 }
653 sa.analyze_stmt(fe.body, iter);
654 });
655 *ctx = post;
656 }
657
658 StmtKind::Switch(sw) => {
660 let _subject_ty = self.expr_analyzer(ctx).analyze(&sw.expr, ctx);
661 let subject_var: Option<String> = match &sw.expr.kind {
663 php_ast::ast::ExprKind::Variable(name) => {
664 Some(name.as_str().trim_start_matches('$').to_string())
665 }
666 _ => None,
667 };
668 let switch_on_true = matches!(&sw.expr.kind, php_ast::ast::ExprKind::Bool(true));
670
671 let pre_ctx = ctx.clone();
672 self.break_ctx_stack.push(Vec::new());
675
676 let has_default = sw.cases.iter().any(|c| c.value.is_none());
677
678 let mut case_results: Vec<Context> = Vec::new();
682 for case in sw.cases.iter() {
683 let mut case_ctx = pre_ctx.fork();
684 if let Some(val) = &case.value {
685 if switch_on_true {
686 narrow_from_condition(
688 val,
689 &mut case_ctx,
690 true,
691 self.codebase,
692 &self.file,
693 );
694 } else if let Some(ref var_name) = subject_var {
695 let narrow_ty = match &val.kind {
697 php_ast::ast::ExprKind::Int(n) => {
698 Some(Union::single(Atomic::TLiteralInt(*n)))
699 }
700 php_ast::ast::ExprKind::String(s) => {
701 Some(Union::single(Atomic::TLiteralString(Arc::from(&**s))))
702 }
703 php_ast::ast::ExprKind::Bool(b) => Some(Union::single(if *b {
704 Atomic::TTrue
705 } else {
706 Atomic::TFalse
707 })),
708 php_ast::ast::ExprKind::Null => Some(Union::single(Atomic::TNull)),
709 _ => None,
710 };
711 if let Some(narrowed) = narrow_ty {
712 case_ctx.set_var(var_name, narrowed);
713 }
714 }
715 self.expr_analyzer(&case_ctx).analyze(val, &mut case_ctx);
716 }
717 for stmt in case.body.iter() {
718 self.analyze_stmt(stmt, &mut case_ctx);
719 }
720 case_results.push(case_ctx);
721 }
722
723 let n = case_results.len();
735 let mut effective_diverges = vec![false; n];
736 for i in (0..n).rev() {
737 if case_results[i].diverges {
738 effective_diverges[i] = true;
739 } else if i + 1 < n {
740 effective_diverges[i] = effective_diverges[i + 1];
742 }
743 }
745
746 let mut all_cases_diverge = true;
749 let mut fallthrough_ctxs: Vec<Context> = Vec::new();
750 for (i, case_ctx) in case_results.into_iter().enumerate() {
751 if !effective_diverges[i] {
752 all_cases_diverge = false;
753 fallthrough_ctxs.push(case_ctx);
754 }
755 }
756
757 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
760
761 let mut merged = if has_default
765 && all_cases_diverge
766 && break_ctxs.is_empty()
767 && fallthrough_ctxs.is_empty()
768 {
769 let mut m = pre_ctx.clone();
771 m.diverges = true;
772 m
773 } else {
774 pre_ctx.clone()
777 };
778
779 for bctx in break_ctxs {
780 merged = Context::merge_branches(&pre_ctx, bctx, Some(merged));
781 }
782 for fctx in fallthrough_ctxs {
783 merged = Context::merge_branches(&pre_ctx, fctx, Some(merged));
784 }
785
786 *ctx = merged;
787 }
788
789 StmtKind::TryCatch(tc) => {
791 let pre_ctx = ctx.clone();
792 let mut try_ctx = ctx.fork();
793 for stmt in tc.body.iter() {
794 self.analyze_stmt(stmt, &mut try_ctx);
795 }
796
797 let catch_base = Context::merge_branches(&pre_ctx, try_ctx.clone(), None);
801
802 let mut non_diverging_catches: Vec<Context> = vec![];
803 for catch in tc.catches.iter() {
804 let mut catch_ctx = catch_base.clone();
805 for catch_ty in catch.types.iter() {
807 self.check_name_undefined_class(catch_ty);
808 }
809 if let Some(var) = catch.var {
810 let exc_ty = if catch.types.is_empty() {
812 Union::single(Atomic::TObject)
813 } else {
814 let mut u = Union::empty();
815 for catch_ty in catch.types.iter() {
816 let raw = crate::parser::name_to_string(catch_ty);
817 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
818 u.add_type(Atomic::TNamedObject {
819 fqcn: resolved.into(),
820 type_params: vec![],
821 });
822 }
823 u
824 };
825 catch_ctx.set_var(var.trim_start_matches('$'), exc_ty);
826 }
827 for stmt in catch.body.iter() {
828 self.analyze_stmt(stmt, &mut catch_ctx);
829 }
830 if !catch_ctx.diverges {
831 non_diverging_catches.push(catch_ctx);
832 }
833 }
834
835 let result = if non_diverging_catches.is_empty() {
839 let mut r = try_ctx;
840 r.diverges = false; r
842 } else {
843 let mut r = try_ctx;
846 for catch_ctx in non_diverging_catches {
847 r = Context::merge_branches(&pre_ctx, r, Some(catch_ctx));
848 }
849 r
850 };
851
852 if let Some(finally_stmts) = &tc.finally {
854 let mut finally_ctx = result.clone();
855 finally_ctx.inside_finally = true;
856 for stmt in finally_stmts.iter() {
857 self.analyze_stmt(stmt, &mut finally_ctx);
858 }
859 }
860
861 *ctx = result;
862 }
863
864 StmtKind::Block(stmts) => {
866 self.analyze_stmts(stmts, ctx);
867 }
868
869 StmtKind::Break(_) => {
871 if let Some(break_ctxs) = self.break_ctx_stack.last_mut() {
874 break_ctxs.push(ctx.clone());
875 }
876 ctx.diverges = true;
879 }
880
881 StmtKind::Continue(_) => {
883 ctx.diverges = true;
886 }
887
888 StmtKind::Unset(vars) => {
890 for var in vars.iter() {
891 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
892 ctx.unset_var(name.as_str().trim_start_matches('$'));
893 }
894 }
895 }
896
897 StmtKind::StaticVar(vars) => {
899 for sv in vars.iter() {
900 let ty = Union::mixed(); ctx.set_var(sv.name.trim_start_matches('$'), ty);
902 }
903 }
904
905 StmtKind::Global(vars) => {
907 for var in vars.iter() {
908 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
909 let var_name = name.as_str().trim_start_matches('$');
910 let ty = self
911 .codebase
912 .global_vars
913 .get(var_name)
914 .map(|r| r.clone())
915 .unwrap_or_else(Union::mixed);
916 ctx.set_var(var_name, ty);
917 }
918 }
919 }
920
921 StmtKind::Declare(d) => {
923 for (name, _val) in d.directives.iter() {
924 if *name == "strict_types" {
925 ctx.strict_types = true;
926 }
927 }
928 if let Some(body) = &d.body {
929 self.analyze_stmt(body, ctx);
930 }
931 }
932
933 StmtKind::Function(decl) => {
935 let params: Vec<mir_codebase::FnParam> = decl
938 .params
939 .iter()
940 .map(|p| mir_codebase::FnParam {
941 name: std::sync::Arc::from(p.name.trim_start_matches('$')),
942 ty: None,
943 default: p.default.as_ref().map(|_| Union::mixed()),
944 is_variadic: p.variadic,
945 is_byref: p.by_ref,
946 is_optional: p.default.is_some() || p.variadic,
947 })
948 .collect();
949 let mut fn_ctx =
950 Context::for_function(¶ms, None, None, None, None, ctx.strict_types, true);
951 let mut sa = StatementsAnalyzer::new(
952 self.codebase,
953 self.file.clone(),
954 self.source,
955 self.source_map,
956 self.issues,
957 self.symbols,
958 self.php_version,
959 );
960 sa.analyze_stmts(&decl.body, &mut fn_ctx);
961 }
962
963 StmtKind::Class(decl) => {
964 let class_name = decl.name.unwrap_or("<anonymous>");
967 let resolved = self.codebase.resolve_class_name(&self.file, class_name);
968 let fqcn: Arc<str> = Arc::from(resolved.as_str());
969 let parent_fqcn = self
970 .codebase
971 .classes
972 .get(fqcn.as_ref())
973 .and_then(|c| c.parent.clone());
974
975 for member in decl.members.iter() {
976 let php_ast::ast::ClassMemberKind::Method(method) = &member.kind else {
977 continue;
978 };
979 let Some(body) = &method.body else { continue };
980 let (params, return_ty) = self
981 .codebase
982 .get_method(fqcn.as_ref(), method.name)
983 .as_deref()
984 .map(|m| (m.params.clone(), m.return_type.clone()))
985 .unwrap_or_else(|| {
986 let ast_params = method
987 .params
988 .iter()
989 .map(|p| mir_codebase::FnParam {
990 name: p.name.trim_start_matches('$').into(),
991 ty: None,
992 default: p.default.as_ref().map(|_| mir_types::Union::mixed()),
993 is_variadic: p.variadic,
994 is_byref: p.by_ref,
995 is_optional: p.default.is_some() || p.variadic,
996 })
997 .collect();
998 (ast_params, None)
999 });
1000 let is_ctor = method.name == "__construct";
1001 let mut method_ctx = Context::for_method(
1002 ¶ms,
1003 return_ty,
1004 Some(fqcn.clone()),
1005 parent_fqcn.clone(),
1006 Some(fqcn.clone()),
1007 ctx.strict_types,
1008 is_ctor,
1009 method.is_static,
1010 );
1011 let mut sa = StatementsAnalyzer::new(
1012 self.codebase,
1013 self.file.clone(),
1014 self.source,
1015 self.source_map,
1016 self.issues,
1017 self.symbols,
1018 self.php_version,
1019 );
1020 sa.analyze_stmts(body, &mut method_ctx);
1021 }
1022 }
1023
1024 StmtKind::Interface(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {
1025 }
1027
1028 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
1030
1031 StmtKind::InlineHtml(_)
1033 | StmtKind::Nop
1034 | StmtKind::Goto(_)
1035 | StmtKind::Label(_)
1036 | StmtKind::HaltCompiler(_) => {}
1037
1038 StmtKind::Error => {}
1039 }
1040 }
1041
1042 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
1047 where
1048 'a: 'b,
1049 {
1050 ExpressionAnalyzer::new(
1051 self.codebase,
1052 self.file.clone(),
1053 self.source,
1054 self.source_map,
1055 self.issues,
1056 self.symbols,
1057 self.php_version,
1058 )
1059 }
1060
1061 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1064 let lc = self.source_map.offset_to_line_col(offset);
1065 let line = lc.line + 1;
1066
1067 let byte_offset = offset as usize;
1068 let line_start_byte = if byte_offset == 0 {
1069 0
1070 } else {
1071 self.source[..byte_offset]
1072 .rfind('\n')
1073 .map(|p| p + 1)
1074 .unwrap_or(0)
1075 };
1076
1077 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1078
1079 (line, col)
1080 }
1081
1082 fn check_name_undefined_class(&mut self, name: &php_ast::ast::Name<'_, '_>) {
1084 let raw = crate::parser::name_to_string(name);
1085 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
1086 if matches!(resolved.as_str(), "self" | "static" | "parent") {
1087 return;
1088 }
1089 if self.codebase.type_exists(&resolved) {
1090 return;
1091 }
1092 let span = name.span();
1093 let (line, col_start) = self.offset_to_line_col(span.start);
1094 let (_, col_end) = self.offset_to_line_col(span.end);
1095 self.issues.add(Issue::new(
1096 IssueKind::UndefinedClass { name: resolved },
1097 Location {
1098 file: self.file.clone(),
1099 line,
1100 col_start,
1101 col_end: col_end.max(col_start + 1),
1102 },
1103 ));
1104 }
1105
1106 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
1113 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
1114 return vec![];
1115 };
1116 let mut suppressions = Vec::new();
1117 for line in doc.lines() {
1118 let line = line.trim().trim_start_matches('*').trim();
1119 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
1120 r
1121 } else if let Some(r) = line.strip_prefix("@suppress ") {
1122 r
1123 } else {
1124 continue;
1125 };
1126 for name in rest.split_whitespace() {
1127 suppressions.push(name.to_string());
1128 }
1129 }
1130 suppressions
1131 }
1132
1133 fn extract_var_annotation(
1137 &self,
1138 span: php_ast::Span,
1139 ) -> Option<(Option<String>, mir_types::Union)> {
1140 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
1141 let parsed = crate::parser::DocblockParser::parse(&doc);
1142 let ty = parsed.var_type?;
1143 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
1144 Some((parsed.var_name, resolved))
1145 }
1146
1147 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
1162 where
1163 F: FnMut(&mut Self, &mut Context),
1164 {
1165 const MAX_ITERS: usize = 3;
1166
1167 self.break_ctx_stack.push(Vec::new());
1169
1170 let mut current = entry;
1171 current.inside_loop = true;
1172
1173 for _ in 0..MAX_ITERS {
1174 let prev_vars = current.vars.clone();
1175
1176 let mut iter = current.clone();
1177 body(self, &mut iter);
1178
1179 let next = Context::merge_branches(pre, iter, None);
1180
1181 if vars_stabilized(&prev_vars, &next.vars) {
1182 current = next;
1183 break;
1184 }
1185 current = next;
1186 }
1187
1188 widen_unstable(&pre.vars, &mut current.vars);
1190
1191 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
1193 for bctx in break_ctxs {
1194 current = Context::merge_branches(pre, current, Some(bctx));
1195 }
1196
1197 current
1198 }
1199}
1200
1201fn vars_stabilized(
1208 prev: &indexmap::IndexMap<String, Union>,
1209 next: &indexmap::IndexMap<String, Union>,
1210) -> bool {
1211 if prev.len() != next.len() {
1212 return false;
1213 }
1214 prev.iter()
1215 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
1216}
1217
1218fn widen_unstable(
1221 pre_vars: &indexmap::IndexMap<String, Union>,
1222 current_vars: &mut indexmap::IndexMap<String, Union>,
1223) {
1224 for (name, ty) in current_vars.iter_mut() {
1225 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
1226 *ty = Union::mixed();
1227 }
1228 }
1229}
1230
1231fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
1236 if arr_ty.is_mixed() {
1237 return (Union::mixed(), Union::mixed());
1238 }
1239 for atomic in &arr_ty.types {
1240 match atomic {
1241 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
1242 return (*key.clone(), *value.clone());
1243 }
1244 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1245 return (Union::single(Atomic::TInt), *value.clone());
1246 }
1247 Atomic::TKeyedArray { properties, .. } => {
1248 let mut keys = Union::empty();
1249 let mut values = Union::empty();
1250 for (k, prop) in properties {
1251 let key_atomic = match k {
1252 ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1253 ArrayKey::Int(i) => Atomic::TLiteralInt(*i),
1254 };
1255 keys = Union::merge(&keys, &Union::single(key_atomic));
1256 values = Union::merge(&values, &prop.ty);
1257 }
1258 let keys = if keys.is_empty() {
1261 Union::mixed()
1262 } else {
1263 keys
1264 };
1265 let values = if values.is_empty() {
1266 Union::mixed()
1267 } else {
1268 values
1269 };
1270 return (keys, values);
1271 }
1272 Atomic::TString => {
1273 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
1274 }
1275 _ => {}
1276 }
1277 }
1278 (Union::mixed(), Union::mixed())
1279}
1280
1281fn named_object_return_compatible(
1288 actual: &Union,
1289 declared: &Union,
1290 codebase: &Codebase,
1291 file: &str,
1292) -> bool {
1293 actual.types.iter().all(|actual_atom| {
1294 let actual_fqcn: &Arc<str> = match actual_atom {
1296 Atomic::TNamedObject { fqcn, .. } => fqcn,
1297 Atomic::TSelf { fqcn } => fqcn,
1298 Atomic::TStaticObject { fqcn } => fqcn,
1299 Atomic::TParent { fqcn } => fqcn,
1300 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1302 Atomic::TVoid => {
1304 return declared
1305 .types
1306 .iter()
1307 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1308 }
1309 Atomic::TNever => return true,
1311 Atomic::TClassString(Some(actual_cls)) => {
1313 return declared.types.iter().any(|d| match d {
1314 Atomic::TClassString(None) => true,
1315 Atomic::TClassString(Some(declared_cls)) => {
1316 actual_cls == declared_cls
1317 || codebase
1318 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1319 }
1320 Atomic::TString => true,
1321 _ => false,
1322 });
1323 }
1324 Atomic::TClassString(None) => {
1325 return declared
1326 .types
1327 .iter()
1328 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1329 }
1330 _ => return false,
1332 };
1333
1334 declared.types.iter().any(|declared_atom| {
1335 let declared_fqcn: &Arc<str> = match declared_atom {
1337 Atomic::TNamedObject { fqcn, .. } => fqcn,
1338 Atomic::TSelf { fqcn } => fqcn,
1339 Atomic::TStaticObject { fqcn } => fqcn,
1340 Atomic::TParent { fqcn } => fqcn,
1341 _ => return false,
1342 };
1343
1344 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1345 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1346
1347 if matches!(
1349 actual_atom,
1350 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1351 ) && (resolved_actual == resolved_declared
1352 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1353 || actual_fqcn.as_ref() == resolved_declared.as_str()
1354 || resolved_actual.as_str() == declared_fqcn.as_ref()
1355 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1356 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1357 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1358 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1359 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1362 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1363 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1364 {
1365 return true;
1366 }
1367
1368 let is_same_class = resolved_actual == resolved_declared
1370 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1371 || actual_fqcn.as_ref() == resolved_declared.as_str()
1372 || resolved_actual.as_str() == declared_fqcn.as_ref();
1373
1374 if is_same_class {
1375 let actual_type_params = match actual_atom {
1376 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1377 _ => &[],
1378 };
1379 let declared_type_params = match declared_atom {
1380 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1381 _ => &[],
1382 };
1383 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1384 let class_tps = codebase.get_class_template_params(&resolved_declared);
1385 return return_type_params_compatible(
1386 actual_type_params,
1387 declared_type_params,
1388 &class_tps,
1389 );
1390 }
1391 return true;
1392 }
1393
1394 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1396 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1397 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1398 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1399 })
1400 })
1401}
1402
1403fn return_type_params_compatible(
1407 actual_params: &[Union],
1408 declared_params: &[Union],
1409 template_params: &[mir_codebase::storage::TemplateParam],
1410) -> bool {
1411 if actual_params.len() != declared_params.len() {
1412 return true;
1413 }
1414 if actual_params.is_empty() {
1415 return true;
1416 }
1417
1418 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1419 {
1420 let variance = template_params
1421 .get(i)
1422 .map(|tp| tp.variance)
1423 .unwrap_or(mir_types::Variance::Invariant);
1424
1425 let compatible = match variance {
1426 mir_types::Variance::Covariant => {
1427 actual_p.is_subtype_of_simple(declared_p)
1428 || declared_p.is_mixed()
1429 || actual_p.is_mixed()
1430 }
1431 mir_types::Variance::Contravariant => {
1432 declared_p.is_subtype_of_simple(actual_p)
1433 || actual_p.is_mixed()
1434 || declared_p.is_mixed()
1435 }
1436 mir_types::Variance::Invariant => {
1437 actual_p == declared_p
1438 || actual_p.is_mixed()
1439 || declared_p.is_mixed()
1440 || (actual_p.is_subtype_of_simple(declared_p)
1441 && declared_p.is_subtype_of_simple(actual_p))
1442 }
1443 };
1444
1445 if !compatible {
1446 return false;
1447 }
1448 }
1449
1450 true
1451}
1452
1453fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1457 declared.types.iter().any(|atomic| match atomic {
1458 Atomic::TTemplateParam { .. } => true,
1459 Atomic::TNamedObject { fqcn, type_params } => {
1465 !type_params.is_empty()
1466 || !codebase.type_exists(fqcn.as_ref())
1467 || codebase.interfaces.contains_key(fqcn.as_ref())
1468 }
1469 Atomic::TArray { value, .. }
1470 | Atomic::TList { value }
1471 | Atomic::TNonEmptyArray { value, .. }
1472 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1473 Atomic::TTemplateParam { .. } => true,
1474 Atomic::TNamedObject { fqcn, .. } => {
1475 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1476 }
1477 _ => false,
1478 }),
1479 _ => false,
1480 })
1481}
1482
1483fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1486 let mut result = Union::empty();
1487 result.possibly_undefined = union.possibly_undefined;
1488 result.from_docblock = union.from_docblock;
1489 for atomic in union.types {
1490 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1491 result.types.push(resolved);
1492 }
1493 result
1494}
1495
1496fn is_resolvable_class_name(s: &str) -> bool {
1497 !s.is_empty()
1498 && s.chars()
1499 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1500}
1501
1502fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1503 match atomic {
1504 Atomic::TNamedObject { fqcn, type_params } => {
1505 if !is_resolvable_class_name(fqcn.as_ref()) {
1506 return Atomic::TNamedObject { fqcn, type_params };
1507 }
1508 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1509 Atomic::TNamedObject {
1510 fqcn: resolved.into(),
1511 type_params,
1512 }
1513 }
1514 Atomic::TClassString(Some(cls)) => {
1515 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1516 Atomic::TClassString(Some(resolved.into()))
1517 }
1518 Atomic::TList { value } => Atomic::TList {
1519 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1520 },
1521 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1522 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1523 },
1524 Atomic::TArray { key, value } => Atomic::TArray {
1525 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1526 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1527 },
1528 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1529 Atomic::TSelf { fqcn }
1531 }
1532 other => other,
1533 }
1534}
1535
1536fn return_arrays_compatible(
1539 actual: &Union,
1540 declared: &Union,
1541 codebase: &Codebase,
1542 file: &str,
1543) -> bool {
1544 actual.types.iter().all(|a_atomic| {
1545 let act_val: &Union = match a_atomic {
1546 Atomic::TArray { value, .. }
1547 | Atomic::TNonEmptyArray { value, .. }
1548 | Atomic::TList { value }
1549 | Atomic::TNonEmptyList { value } => value,
1550 Atomic::TKeyedArray { .. } => return true,
1551 _ => return false,
1552 };
1553
1554 declared.types.iter().any(|d_atomic| {
1555 let dec_val: &Union = match d_atomic {
1556 Atomic::TArray { value, .. }
1557 | Atomic::TNonEmptyArray { value, .. }
1558 | Atomic::TList { value }
1559 | Atomic::TNonEmptyList { value } => value,
1560 _ => return false,
1561 };
1562
1563 act_val.types.iter().all(|av| {
1564 match av {
1565 Atomic::TNever => return true,
1566 Atomic::TClassString(Some(av_cls)) => {
1567 return dec_val.types.iter().any(|dv| match dv {
1568 Atomic::TClassString(None) | Atomic::TString => true,
1569 Atomic::TClassString(Some(dv_cls)) => {
1570 av_cls == dv_cls
1571 || codebase
1572 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1573 }
1574 _ => false,
1575 });
1576 }
1577 _ => {}
1578 }
1579 let av_fqcn: &Arc<str> = match av {
1580 Atomic::TNamedObject { fqcn, .. } => fqcn,
1581 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1582 Atomic::TClosure { .. } => return true,
1583 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1584 };
1585 dec_val.types.iter().any(|dv| {
1586 let dv_fqcn: &Arc<str> = match dv {
1587 Atomic::TNamedObject { fqcn, .. } => fqcn,
1588 Atomic::TClosure { .. } => return true,
1589 _ => return false,
1590 };
1591 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1592 return true; }
1594 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1595 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1596 res_dec == res_act
1597 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1598 || codebase.extends_or_implements(&res_act, &res_dec)
1599 })
1600 })
1601 })
1602 })
1603}