1use std::sync::Arc;
4
5use php_ast::ast::StmtKind;
6
7use mir_codebase::Codebase;
8use mir_issues::{IssueBuffer, IssueKind};
9use mir_types::{Atomic, Union};
10
11use crate::context::Context;
12use crate::expr::ExpressionAnalyzer;
13use crate::narrowing::narrow_from_condition;
14use crate::symbol::ResolvedSymbol;
15
16pub struct StatementsAnalyzer<'a> {
21 pub codebase: &'a Codebase,
22 pub file: Arc<str>,
23 pub source: &'a str,
24 pub source_map: &'a php_rs_parser::source_map::SourceMap,
25 pub issues: &'a mut IssueBuffer,
26 pub symbols: &'a mut Vec<ResolvedSymbol>,
27 pub return_types: Vec<Union>,
29 break_ctx_stack: Vec<Vec<Context>>,
32}
33
34impl<'a> StatementsAnalyzer<'a> {
35 pub fn new(
36 codebase: &'a Codebase,
37 file: Arc<str>,
38 source: &'a str,
39 source_map: &'a php_rs_parser::source_map::SourceMap,
40 issues: &'a mut IssueBuffer,
41 symbols: &'a mut Vec<ResolvedSymbol>,
42 ) -> Self {
43 Self {
44 codebase,
45 file,
46 source,
47 source_map,
48 issues,
49 symbols,
50 return_types: Vec::new(),
51 break_ctx_stack: Vec::new(),
52 }
53 }
54
55 pub fn analyze_stmts<'arena, 'src>(
56 &mut self,
57 stmts: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Stmt<'arena, 'src>>,
58 ctx: &mut Context,
59 ) {
60 for stmt in stmts.iter() {
61 let suppressions = self.extract_statement_suppressions(stmt.span);
63 let before = self.issues.issue_count();
64
65 let var_annotation = self.extract_var_annotation(stmt.span);
67
68 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
71 ctx.set_var(var_name.as_str(), var_ty.clone());
72 }
73
74 self.analyze_stmt(stmt, ctx);
75
76 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
80 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
81 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
82 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
83 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
84 let lhs = lhs_name.trim_start_matches('$');
85 if lhs == var_name.as_str() {
86 ctx.set_var(var_name.as_str(), var_ty.clone());
87 }
88 }
89 }
90 }
91 }
92 }
93
94 if !suppressions.is_empty() {
95 self.issues.suppress_range(before, &suppressions);
96 }
97 }
98 }
99
100 pub fn analyze_stmt<'arena, 'src>(
101 &mut self,
102 stmt: &php_ast::ast::Stmt<'arena, 'src>,
103 ctx: &mut Context,
104 ) {
105 match &stmt.kind {
106 StmtKind::Expression(expr) => {
108 self.expr_analyzer(ctx).analyze(expr, ctx);
109 if let php_ast::ast::ExprKind::FunctionCall(call) = &expr.kind {
111 if let php_ast::ast::ExprKind::Identifier(fn_name) = &call.name.kind {
112 if fn_name.eq_ignore_ascii_case("assert") {
113 if let Some(arg) = call.args.first() {
114 narrow_from_condition(
115 &arg.value,
116 ctx,
117 true,
118 self.codebase,
119 &self.file,
120 );
121 }
122 }
123 }
124 }
125 }
126
127 StmtKind::Echo(exprs) => {
129 for expr in exprs.iter() {
130 if crate::taint::is_expr_tainted(expr, ctx) {
132 let (line, col_start) = self.offset_to_line_col_utf16(stmt.span.start);
133 let col_end = if stmt.span.start < stmt.span.end {
134 let (_end_line, end_col) = self.offset_to_line_col_utf16(stmt.span.end);
135 end_col
136 } else {
137 col_start
138 };
139 let mut issue = mir_issues::Issue::new(
140 IssueKind::TaintedHtml,
141 mir_issues::Location {
142 file: self.file.clone(),
143 line,
144 col_start,
145 col_end: col_end.max(col_start + 1),
146 },
147 );
148 let start = stmt.span.start as usize;
150 let end = stmt.span.end as usize;
151 if start < self.source.len() {
152 let end = end.min(self.source.len());
153 let span_text = &self.source[start..end];
154 if let Some(first_line) = span_text.lines().next() {
155 issue = issue.with_snippet(first_line.trim().to_string());
156 }
157 }
158 self.issues.add(issue);
159 }
160 self.expr_analyzer(ctx).analyze(expr, ctx);
161 }
162 }
163
164 StmtKind::Return(opt_expr) => {
166 if let Some(expr) = opt_expr {
167 let ret_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
168
169 let check_ty =
174 if let Some((None, var_ty)) = self.extract_var_annotation(stmt.span) {
175 var_ty
176 } else {
177 ret_ty.clone()
178 };
179
180 if let Some(declared) = &ctx.fn_return_type.clone() {
182 if (declared.is_void() && !check_ty.is_void() && !check_ty.is_mixed())
186 || (!check_ty.is_subtype_of_simple(declared)
187 && !declared.is_mixed()
188 && !check_ty.is_mixed()
189 && !named_object_return_compatible(&check_ty, declared, self.codebase, &self.file)
190 && (check_ty.remove_null().is_empty() || !named_object_return_compatible(&check_ty.remove_null(), declared, self.codebase, &self.file))
194 && !declared_return_has_template(declared, self.codebase)
195 && !declared_return_has_template(&check_ty, self.codebase)
196 && !return_arrays_compatible(&check_ty, declared, self.codebase, &self.file)
197 && !declared.is_subtype_of_simple(&check_ty)
199 && !declared.remove_null().is_subtype_of_simple(&check_ty)
200 && (check_ty.remove_null().is_empty() || !check_ty.remove_null().is_subtype_of_simple(declared))
205 && !check_ty.remove_false().is_subtype_of_simple(declared)
206 && !named_object_return_compatible(declared, &check_ty, self.codebase, &self.file)
209 && !named_object_return_compatible(&declared.remove_null(), &check_ty.remove_null(), self.codebase, &self.file))
210 {
211 let (line, col_start) = self.offset_to_line_col_utf16(stmt.span.start);
212 let col_end = if stmt.span.start < stmt.span.end {
213 let (_end_line, end_col) =
214 self.offset_to_line_col_utf16(stmt.span.end);
215 end_col
216 } else {
217 col_start
218 };
219 self.issues.add(
220 mir_issues::Issue::new(
221 IssueKind::InvalidReturnType {
222 expected: format!("{}", declared),
223 actual: format!("{}", ret_ty),
224 },
225 mir_issues::Location {
226 file: self.file.clone(),
227 line,
228 col_start,
229 col_end: col_end.max(col_start + 1),
230 },
231 )
232 .with_snippet(
233 crate::parser::span_text(self.source, stmt.span)
234 .unwrap_or_default(),
235 ),
236 );
237 }
238 }
239 self.return_types.push(ret_ty);
240 } else {
241 self.return_types.push(Union::single(Atomic::TVoid));
242 if let Some(declared) = &ctx.fn_return_type.clone() {
244 if !declared.is_void() && !declared.is_mixed() {
245 let (line, col_start) = self.offset_to_line_col_utf16(stmt.span.start);
246 let col_end = if stmt.span.start < stmt.span.end {
247 let (_end_line, end_col) =
248 self.offset_to_line_col_utf16(stmt.span.end);
249 end_col
250 } else {
251 col_start
252 };
253 self.issues.add(
254 mir_issues::Issue::new(
255 IssueKind::InvalidReturnType {
256 expected: format!("{}", declared),
257 actual: "void".to_string(),
258 },
259 mir_issues::Location {
260 file: self.file.clone(),
261 line,
262 col_start,
263 col_end: col_end.max(col_start + 1),
264 },
265 )
266 .with_snippet(
267 crate::parser::span_text(self.source, stmt.span)
268 .unwrap_or_default(),
269 ),
270 );
271 }
272 }
273 }
274 ctx.diverges = true;
275 }
276
277 StmtKind::Throw(expr) => {
279 let thrown_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
280 for atomic in &thrown_ty.types {
282 match atomic {
283 mir_types::Atomic::TNamedObject { fqcn, .. } => {
284 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
285 let is_throwable = resolved == "Throwable"
286 || resolved == "Exception"
287 || resolved == "Error"
288 || fqcn.as_ref() == "Throwable"
289 || fqcn.as_ref() == "Exception"
290 || fqcn.as_ref() == "Error"
291 || self.codebase.extends_or_implements(&resolved, "Throwable")
292 || self.codebase.extends_or_implements(&resolved, "Exception")
293 || self.codebase.extends_or_implements(&resolved, "Error")
294 || self.codebase.extends_or_implements(fqcn, "Throwable")
295 || self.codebase.extends_or_implements(fqcn, "Exception")
296 || self.codebase.extends_or_implements(fqcn, "Error")
297 || self.codebase.has_unknown_ancestor(&resolved)
299 || self.codebase.has_unknown_ancestor(fqcn)
300 || (!self.codebase.type_exists(&resolved) && !self.codebase.type_exists(fqcn));
302 if !is_throwable {
303 let (line, col_start) =
304 self.offset_to_line_col_utf16(stmt.span.start);
305 let col_end = if stmt.span.start < stmt.span.end {
306 let (_end_line, end_col) =
307 self.offset_to_line_col_utf16(stmt.span.end);
308 end_col
309 } else {
310 col_start
311 };
312 self.issues.add(mir_issues::Issue::new(
313 IssueKind::InvalidThrow {
314 ty: fqcn.to_string(),
315 },
316 mir_issues::Location {
317 file: self.file.clone(),
318 line,
319 col_start,
320 col_end: col_end.max(col_start + 1),
321 },
322 ));
323 }
324 }
325 mir_types::Atomic::TSelf { fqcn }
327 | mir_types::Atomic::TStaticObject { fqcn }
328 | mir_types::Atomic::TParent { fqcn } => {
329 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
330 let is_throwable = resolved == "Throwable"
331 || resolved == "Exception"
332 || resolved == "Error"
333 || self.codebase.extends_or_implements(&resolved, "Throwable")
334 || self.codebase.extends_or_implements(&resolved, "Exception")
335 || self.codebase.extends_or_implements(&resolved, "Error")
336 || self.codebase.extends_or_implements(fqcn, "Throwable")
337 || self.codebase.extends_or_implements(fqcn, "Exception")
338 || self.codebase.extends_or_implements(fqcn, "Error")
339 || self.codebase.has_unknown_ancestor(&resolved)
340 || self.codebase.has_unknown_ancestor(fqcn);
341 if !is_throwable {
342 let (line, col_start) =
343 self.offset_to_line_col_utf16(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_utf16(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_utf16(stmt.span.start);
367 let col_end = if stmt.span.start < stmt.span.end {
368 let (_end_line, end_col) =
369 self.offset_to_line_col_utf16(stmt.span.end);
370 end_col
371 } else {
372 col_start
373 };
374 self.issues.add(mir_issues::Issue::new(
375 IssueKind::InvalidThrow {
376 ty: format!("{}", thrown_ty),
377 },
378 mir_issues::Location {
379 file: self.file.clone(),
380 line,
381 col_start,
382 col_end: col_end.max(col_start + 1),
383 },
384 ));
385 }
386 }
387 }
388 ctx.diverges = true;
389 }
390
391 StmtKind::If(if_stmt) => {
393 let pre_ctx = ctx.clone();
394
395 let cond_type = self.expr_analyzer(ctx).analyze(&if_stmt.condition, ctx);
397 let pre_diverges = ctx.diverges;
398
399 let mut then_ctx = ctx.fork();
401 narrow_from_condition(
402 &if_stmt.condition,
403 &mut then_ctx,
404 true,
405 self.codebase,
406 &self.file,
407 );
408 if !then_ctx.diverges {
411 self.analyze_stmt(if_stmt.then_branch, &mut then_ctx);
412 }
413
414 let mut elseif_ctxs: Vec<Context> = vec![];
416 for elseif in if_stmt.elseif_branches.iter() {
417 let mut branch_ctx = ctx.fork();
418 narrow_from_condition(
419 &elseif.condition,
420 &mut branch_ctx,
421 true,
422 self.codebase,
423 &self.file,
424 );
425 self.expr_analyzer(&branch_ctx)
426 .analyze(&elseif.condition, &mut branch_ctx);
427 if !branch_ctx.diverges {
428 self.analyze_stmt(&elseif.body, &mut branch_ctx);
429 }
430 elseif_ctxs.push(branch_ctx);
431 }
432
433 let mut else_ctx = ctx.fork();
435 narrow_from_condition(
436 &if_stmt.condition,
437 &mut else_ctx,
438 false,
439 self.codebase,
440 &self.file,
441 );
442 if !else_ctx.diverges {
443 if let Some(else_branch) = &if_stmt.else_branch {
444 self.analyze_stmt(else_branch, &mut else_ctx);
445 }
446 }
447
448 if !pre_diverges && (then_ctx.diverges || else_ctx.diverges) {
450 let (line, col_start) =
451 self.offset_to_line_col_utf16(if_stmt.condition.span.start);
452 let col_end = if if_stmt.condition.span.start < if_stmt.condition.span.end {
453 let (_end_line, end_col) =
454 self.offset_to_line_col_utf16(if_stmt.condition.span.end);
455 end_col
456 } else {
457 col_start
458 };
459 self.issues.add(
460 mir_issues::Issue::new(
461 IssueKind::RedundantCondition {
462 ty: format!("{}", cond_type),
463 },
464 mir_issues::Location {
465 file: self.file.clone(),
466 line,
467 col_start,
468 col_end: col_end.max(col_start + 1),
469 },
470 )
471 .with_snippet(
472 crate::parser::span_text(self.source, if_stmt.condition.span)
473 .unwrap_or_default(),
474 ),
475 );
476 }
477
478 *ctx = Context::merge_branches(&pre_ctx, then_ctx, Some(else_ctx));
480 for ec in elseif_ctxs {
481 *ctx = Context::merge_branches(&pre_ctx, ec, None);
482 }
483 }
484
485 StmtKind::While(w) => {
487 self.expr_analyzer(ctx).analyze(&w.condition, ctx);
488 let pre = ctx.clone();
489
490 let mut entry = ctx.fork();
492 narrow_from_condition(&w.condition, &mut entry, true, self.codebase, &self.file);
493
494 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
495 sa.analyze_stmt(w.body, iter);
496 sa.expr_analyzer(iter).analyze(&w.condition, iter);
497 });
498 *ctx = post;
499 }
500
501 StmtKind::DoWhile(dw) => {
503 let pre = ctx.clone();
504 let entry = ctx.fork();
505 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
506 sa.analyze_stmt(dw.body, iter);
507 sa.expr_analyzer(iter).analyze(&dw.condition, iter);
508 });
509 *ctx = post;
510 }
511
512 StmtKind::For(f) => {
514 for init in f.init.iter() {
516 self.expr_analyzer(ctx).analyze(init, ctx);
517 }
518 let pre = ctx.clone();
519 let mut entry = ctx.fork();
520 for cond in f.condition.iter() {
521 self.expr_analyzer(&entry).analyze(cond, &mut entry);
522 }
523
524 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
525 sa.analyze_stmt(f.body, iter);
526 for update in f.update.iter() {
527 sa.expr_analyzer(iter).analyze(update, iter);
528 }
529 for cond in f.condition.iter() {
530 sa.expr_analyzer(iter).analyze(cond, iter);
531 }
532 });
533 *ctx = post;
534 }
535
536 StmtKind::Foreach(fe) => {
538 let arr_ty = self.expr_analyzer(ctx).analyze(&fe.expr, ctx);
539 let (key_ty, mut value_ty) = infer_foreach_types(&arr_ty);
540
541 if let Some(vname) = crate::expr::extract_simple_var(&fe.value) {
544 if let Some((Some(ann_var), ann_ty)) = self.extract_var_annotation(stmt.span) {
545 if ann_var == vname {
546 value_ty = ann_ty;
547 }
548 }
549 }
550
551 let pre = ctx.clone();
552 let mut entry = ctx.fork();
553
554 if let Some(key_expr) = &fe.key {
556 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
557 entry.set_var(var_name, key_ty.clone());
558 }
559 }
560 let value_var = crate::expr::extract_simple_var(&fe.value);
563 let value_destructure_vars = crate::expr::extract_destructure_vars(&fe.value);
564 if let Some(ref vname) = value_var {
565 entry.set_var(vname.as_str(), value_ty.clone());
566 } else {
567 for vname in &value_destructure_vars {
568 entry.set_var(vname, Union::mixed());
569 }
570 }
571
572 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
573 if let Some(key_expr) = &fe.key {
575 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
576 iter.set_var(var_name, key_ty.clone());
577 }
578 }
579 if let Some(ref vname) = value_var {
580 iter.set_var(vname.as_str(), value_ty.clone());
581 } else {
582 for vname in &value_destructure_vars {
583 iter.set_var(vname, Union::mixed());
584 }
585 }
586 sa.analyze_stmt(fe.body, iter);
587 });
588 *ctx = post;
589 }
590
591 StmtKind::Switch(sw) => {
593 let _subject_ty = self.expr_analyzer(ctx).analyze(&sw.expr, ctx);
594 let subject_var: Option<String> = match &sw.expr.kind {
596 php_ast::ast::ExprKind::Variable(name) => {
597 Some(name.as_str().trim_start_matches('$').to_string())
598 }
599 _ => None,
600 };
601 let switch_on_true = matches!(&sw.expr.kind, php_ast::ast::ExprKind::Bool(true));
603
604 let pre_ctx = ctx.clone();
605 self.break_ctx_stack.push(Vec::new());
608
609 let mut all_cases_diverge = true;
610 let has_default = sw.cases.iter().any(|c| c.value.is_none());
611
612 for case in sw.cases.iter() {
613 let mut case_ctx = pre_ctx.fork();
614 if let Some(val) = &case.value {
615 if switch_on_true {
616 narrow_from_condition(
618 val,
619 &mut case_ctx,
620 true,
621 self.codebase,
622 &self.file,
623 );
624 } else if let Some(ref var_name) = subject_var {
625 let narrow_ty = match &val.kind {
627 php_ast::ast::ExprKind::Int(n) => {
628 Some(Union::single(Atomic::TLiteralInt(*n)))
629 }
630 php_ast::ast::ExprKind::String(s) => {
631 Some(Union::single(Atomic::TLiteralString(Arc::from(&**s))))
632 }
633 php_ast::ast::ExprKind::Bool(b) => Some(Union::single(if *b {
634 Atomic::TTrue
635 } else {
636 Atomic::TFalse
637 })),
638 php_ast::ast::ExprKind::Null => Some(Union::single(Atomic::TNull)),
639 _ => None,
640 };
641 if let Some(narrowed) = narrow_ty {
642 case_ctx.set_var(var_name, narrowed);
643 }
644 }
645 self.expr_analyzer(&case_ctx).analyze(val, &mut case_ctx);
646 }
647 for stmt in case.body.iter() {
648 self.analyze_stmt(stmt, &mut case_ctx);
649 }
650 if !case_ctx.diverges {
651 all_cases_diverge = false;
652 }
653 }
654
655 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
658
659 let mut merged = if has_default && all_cases_diverge && break_ctxs.is_empty() {
663 let mut m = pre_ctx.clone();
665 m.diverges = true;
666 m
667 } else {
668 pre_ctx.clone()
671 };
672
673 for bctx in break_ctxs {
674 merged = Context::merge_branches(&pre_ctx, bctx, Some(merged));
675 }
676
677 *ctx = merged;
678 }
679
680 StmtKind::TryCatch(tc) => {
682 let pre_ctx = ctx.clone();
683 let mut try_ctx = ctx.fork();
684 for stmt in tc.body.iter() {
685 self.analyze_stmt(stmt, &mut try_ctx);
686 }
687
688 let catch_base = Context::merge_branches(&pre_ctx, try_ctx.clone(), None);
692
693 let mut non_diverging_catches: Vec<Context> = vec![];
694 for catch in tc.catches.iter() {
695 let mut catch_ctx = catch_base.clone();
696 if let Some(var) = catch.var {
697 let exc_ty = if catch.types.is_empty() {
699 Union::single(Atomic::TObject)
700 } else {
701 let mut u = Union::empty();
702 for catch_ty in catch.types.iter() {
703 let raw = crate::parser::name_to_string(catch_ty);
704 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
705 u.add_type(Atomic::TNamedObject {
706 fqcn: resolved.into(),
707 type_params: vec![],
708 });
709 }
710 u
711 };
712 catch_ctx.set_var(var.trim_start_matches('$'), exc_ty);
713 }
714 for stmt in catch.body.iter() {
715 self.analyze_stmt(stmt, &mut catch_ctx);
716 }
717 if !catch_ctx.diverges {
718 non_diverging_catches.push(catch_ctx);
719 }
720 }
721
722 let result = if non_diverging_catches.is_empty() {
726 let mut r = try_ctx;
727 r.diverges = false; r
729 } else {
730 let mut r = try_ctx;
733 for catch_ctx in non_diverging_catches {
734 r = Context::merge_branches(&pre_ctx, r, Some(catch_ctx));
735 }
736 r
737 };
738
739 if let Some(finally_stmts) = &tc.finally {
741 let mut finally_ctx = result.clone();
742 finally_ctx.inside_finally = true;
743 for stmt in finally_stmts.iter() {
744 self.analyze_stmt(stmt, &mut finally_ctx);
745 }
746 }
747
748 *ctx = result;
749 }
750
751 StmtKind::Block(stmts) => {
753 self.analyze_stmts(stmts, ctx);
754 }
755
756 StmtKind::Break(_) => {
758 if let Some(break_ctxs) = self.break_ctx_stack.last_mut() {
761 break_ctxs.push(ctx.clone());
762 }
763 ctx.diverges = true;
766 }
767
768 StmtKind::Continue(_) => {
770 ctx.diverges = true;
773 }
774
775 StmtKind::Unset(vars) => {
777 for var in vars.iter() {
778 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
779 ctx.unset_var(name.as_str().trim_start_matches('$'));
780 }
781 }
782 }
783
784 StmtKind::StaticVar(vars) => {
786 for sv in vars.iter() {
787 let ty = Union::mixed(); ctx.set_var(sv.name.trim_start_matches('$'), ty);
789 }
790 }
791
792 StmtKind::Global(vars) => {
794 for var in vars.iter() {
795 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
796 let var_name = name.as_str().trim_start_matches('$');
797 let ty = self
798 .codebase
799 .global_vars
800 .get(var_name)
801 .map(|r| r.clone())
802 .unwrap_or_else(Union::mixed);
803 ctx.set_var(var_name, ty);
804 }
805 }
806 }
807
808 StmtKind::Declare(d) => {
810 for (name, _val) in d.directives.iter() {
811 if *name == "strict_types" {
812 ctx.strict_types = true;
813 }
814 }
815 if let Some(body) = &d.body {
816 self.analyze_stmt(body, ctx);
817 }
818 }
819
820 StmtKind::Function(_)
822 | StmtKind::Class(_)
823 | StmtKind::Interface(_)
824 | StmtKind::Trait(_)
825 | StmtKind::Enum(_) => {
826 }
828
829 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
831
832 StmtKind::InlineHtml(_)
834 | StmtKind::Nop
835 | StmtKind::Goto(_)
836 | StmtKind::Label(_)
837 | StmtKind::HaltCompiler(_) => {}
838
839 StmtKind::Error => {}
840 }
841 }
842
843 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
848 where
849 'a: 'b,
850 {
851 ExpressionAnalyzer::new(
852 self.codebase,
853 self.file.clone(),
854 self.source,
855 self.source_map,
856 self.issues,
857 self.symbols,
858 )
859 }
860
861 fn offset_to_line_col_utf16(&self, offset: u32) -> (u32, u16) {
864 let lc = self.source_map.offset_to_line_col(offset);
865 let line = lc.line + 1;
866
867 let byte_offset = offset as usize;
869 let line_start_byte = if byte_offset == 0 {
870 0
871 } else {
872 self.source[..byte_offset]
874 .rfind('\n')
875 .map(|p| p + 1)
876 .unwrap_or(0)
877 };
878
879 let col_utf16 = self.source[line_start_byte..byte_offset]
881 .chars()
882 .map(|c| c.len_utf16() as u16)
883 .sum();
884
885 (line, col_utf16)
886 }
887
888 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
895 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
896 return vec![];
897 };
898 let mut suppressions = Vec::new();
899 for line in doc.lines() {
900 let line = line.trim().trim_start_matches('*').trim();
901 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
902 r
903 } else if let Some(r) = line.strip_prefix("@suppress ") {
904 r
905 } else {
906 continue;
907 };
908 for name in rest.split_whitespace() {
909 suppressions.push(name.to_string());
910 }
911 }
912 suppressions
913 }
914
915 fn extract_var_annotation(
919 &self,
920 span: php_ast::Span,
921 ) -> Option<(Option<String>, mir_types::Union)> {
922 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
923 let parsed = crate::parser::DocblockParser::parse(&doc);
924 let ty = parsed.var_type?;
925 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
926 Some((parsed.var_name, resolved))
927 }
928
929 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
944 where
945 F: FnMut(&mut Self, &mut Context),
946 {
947 const MAX_ITERS: usize = 3;
948
949 self.break_ctx_stack.push(Vec::new());
951
952 let mut current = entry;
953 current.inside_loop = true;
954
955 for _ in 0..MAX_ITERS {
956 let prev_vars = current.vars.clone();
957
958 let mut iter = current.clone();
959 body(self, &mut iter);
960
961 let next = Context::merge_branches(pre, iter, None);
962
963 if vars_stabilized(&prev_vars, &next.vars) {
964 current = next;
965 break;
966 }
967 current = next;
968 }
969
970 widen_unstable(&pre.vars, &mut current.vars);
972
973 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
975 for bctx in break_ctxs {
976 current = Context::merge_branches(pre, current, Some(bctx));
977 }
978
979 current
980 }
981}
982
983fn vars_stabilized(
990 prev: &indexmap::IndexMap<String, Union>,
991 next: &indexmap::IndexMap<String, Union>,
992) -> bool {
993 if prev.len() != next.len() {
994 return false;
995 }
996 prev.iter()
997 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
998}
999
1000fn widen_unstable(
1003 pre_vars: &indexmap::IndexMap<String, Union>,
1004 current_vars: &mut indexmap::IndexMap<String, Union>,
1005) {
1006 for (name, ty) in current_vars.iter_mut() {
1007 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
1008 *ty = Union::mixed();
1009 }
1010 }
1011}
1012
1013fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
1018 if arr_ty.is_mixed() {
1019 return (Union::mixed(), Union::mixed());
1020 }
1021 for atomic in &arr_ty.types {
1022 match atomic {
1023 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
1024 return (*key.clone(), *value.clone());
1025 }
1026 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1027 return (Union::single(Atomic::TInt), *value.clone());
1028 }
1029 Atomic::TKeyedArray { properties, .. } => {
1030 let mut values = Union::empty();
1031 for (_k, prop) in properties {
1032 values = Union::merge(&values, &prop.ty);
1033 }
1034 let values = if values.is_empty() {
1037 Union::mixed()
1038 } else {
1039 values
1040 };
1041 return (Union::single(Atomic::TMixed), values);
1042 }
1043 Atomic::TString => {
1044 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
1045 }
1046 _ => {}
1047 }
1048 }
1049 (Union::mixed(), Union::mixed())
1050}
1051
1052fn named_object_return_compatible(
1059 actual: &Union,
1060 declared: &Union,
1061 codebase: &Codebase,
1062 file: &str,
1063) -> bool {
1064 actual.types.iter().all(|actual_atom| {
1065 let actual_fqcn: &Arc<str> = match actual_atom {
1067 Atomic::TNamedObject { fqcn, .. } => fqcn,
1068 Atomic::TSelf { fqcn } => fqcn,
1069 Atomic::TStaticObject { fqcn } => fqcn,
1070 Atomic::TParent { fqcn } => fqcn,
1071 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1073 Atomic::TVoid => {
1075 return declared
1076 .types
1077 .iter()
1078 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1079 }
1080 Atomic::TNever => return true,
1082 Atomic::TClassString(Some(actual_cls)) => {
1084 return declared.types.iter().any(|d| match d {
1085 Atomic::TClassString(None) => true,
1086 Atomic::TClassString(Some(declared_cls)) => {
1087 actual_cls == declared_cls
1088 || codebase
1089 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1090 }
1091 Atomic::TString => true,
1092 _ => false,
1093 });
1094 }
1095 Atomic::TClassString(None) => {
1096 return declared
1097 .types
1098 .iter()
1099 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1100 }
1101 _ => return false,
1103 };
1104
1105 declared.types.iter().any(|declared_atom| {
1106 let declared_fqcn: &Arc<str> = match declared_atom {
1108 Atomic::TNamedObject { fqcn, .. } => fqcn,
1109 Atomic::TSelf { fqcn } => fqcn,
1110 Atomic::TStaticObject { fqcn } => fqcn,
1111 Atomic::TParent { fqcn } => fqcn,
1112 _ => return false,
1113 };
1114
1115 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1116 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1117
1118 if matches!(
1120 actual_atom,
1121 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1122 ) && (resolved_actual == resolved_declared
1123 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1124 || actual_fqcn.as_ref() == resolved_declared.as_str()
1125 || resolved_actual.as_str() == declared_fqcn.as_ref()
1126 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1127 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1128 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1129 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1130 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1133 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1134 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1135 {
1136 return true;
1137 }
1138
1139 let is_same_class = resolved_actual == resolved_declared
1141 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1142 || actual_fqcn.as_ref() == resolved_declared.as_str()
1143 || resolved_actual.as_str() == declared_fqcn.as_ref();
1144
1145 if is_same_class {
1146 let actual_type_params = match actual_atom {
1147 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1148 _ => &[],
1149 };
1150 let declared_type_params = match declared_atom {
1151 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1152 _ => &[],
1153 };
1154 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1155 let class_tps = codebase.get_class_template_params(&resolved_declared);
1156 return return_type_params_compatible(
1157 actual_type_params,
1158 declared_type_params,
1159 &class_tps,
1160 );
1161 }
1162 return true;
1163 }
1164
1165 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1167 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1168 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1169 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1170 })
1171 })
1172}
1173
1174fn return_type_params_compatible(
1178 actual_params: &[Union],
1179 declared_params: &[Union],
1180 template_params: &[mir_codebase::storage::TemplateParam],
1181) -> bool {
1182 if actual_params.len() != declared_params.len() {
1183 return true;
1184 }
1185 if actual_params.is_empty() {
1186 return true;
1187 }
1188
1189 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1190 {
1191 let variance = template_params
1192 .get(i)
1193 .map(|tp| tp.variance)
1194 .unwrap_or(mir_types::Variance::Invariant);
1195
1196 let compatible = match variance {
1197 mir_types::Variance::Covariant => {
1198 actual_p.is_subtype_of_simple(declared_p)
1199 || declared_p.is_mixed()
1200 || actual_p.is_mixed()
1201 }
1202 mir_types::Variance::Contravariant => {
1203 declared_p.is_subtype_of_simple(actual_p)
1204 || actual_p.is_mixed()
1205 || declared_p.is_mixed()
1206 }
1207 mir_types::Variance::Invariant => {
1208 actual_p == declared_p
1209 || actual_p.is_mixed()
1210 || declared_p.is_mixed()
1211 || (actual_p.is_subtype_of_simple(declared_p)
1212 && declared_p.is_subtype_of_simple(actual_p))
1213 }
1214 };
1215
1216 if !compatible {
1217 return false;
1218 }
1219 }
1220
1221 true
1222}
1223
1224fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1228 declared.types.iter().any(|atomic| match atomic {
1229 Atomic::TTemplateParam { .. } => true,
1230 Atomic::TNamedObject { fqcn, type_params } => {
1236 !type_params.is_empty()
1237 || !codebase.type_exists(fqcn.as_ref())
1238 || codebase.interfaces.contains_key(fqcn.as_ref())
1239 }
1240 Atomic::TArray { value, .. }
1241 | Atomic::TList { value }
1242 | Atomic::TNonEmptyArray { value, .. }
1243 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1244 Atomic::TTemplateParam { .. } => true,
1245 Atomic::TNamedObject { fqcn, .. } => {
1246 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1247 }
1248 _ => false,
1249 }),
1250 _ => false,
1251 })
1252}
1253
1254fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1257 let mut result = Union::empty();
1258 result.possibly_undefined = union.possibly_undefined;
1259 result.from_docblock = union.from_docblock;
1260 for atomic in union.types {
1261 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1262 result.types.push(resolved);
1263 }
1264 result
1265}
1266
1267fn is_resolvable_class_name(s: &str) -> bool {
1268 !s.is_empty()
1269 && s.chars()
1270 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1271}
1272
1273fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1274 match atomic {
1275 Atomic::TNamedObject { fqcn, type_params } => {
1276 if !is_resolvable_class_name(fqcn.as_ref()) {
1277 return Atomic::TNamedObject { fqcn, type_params };
1278 }
1279 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1280 Atomic::TNamedObject {
1281 fqcn: resolved.into(),
1282 type_params,
1283 }
1284 }
1285 Atomic::TClassString(Some(cls)) => {
1286 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1287 Atomic::TClassString(Some(resolved.into()))
1288 }
1289 Atomic::TList { value } => Atomic::TList {
1290 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1291 },
1292 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1293 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1294 },
1295 Atomic::TArray { key, value } => Atomic::TArray {
1296 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1297 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1298 },
1299 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1300 Atomic::TSelf { fqcn }
1302 }
1303 other => other,
1304 }
1305}
1306
1307fn return_arrays_compatible(
1310 actual: &Union,
1311 declared: &Union,
1312 codebase: &Codebase,
1313 file: &str,
1314) -> bool {
1315 actual.types.iter().all(|a_atomic| {
1316 let act_val: &Union = match a_atomic {
1317 Atomic::TArray { value, .. }
1318 | Atomic::TNonEmptyArray { value, .. }
1319 | Atomic::TList { value }
1320 | Atomic::TNonEmptyList { value } => value,
1321 Atomic::TKeyedArray { .. } => return true,
1322 _ => return false,
1323 };
1324
1325 declared.types.iter().any(|d_atomic| {
1326 let dec_val: &Union = match d_atomic {
1327 Atomic::TArray { value, .. }
1328 | Atomic::TNonEmptyArray { value, .. }
1329 | Atomic::TList { value }
1330 | Atomic::TNonEmptyList { value } => value,
1331 _ => return false,
1332 };
1333
1334 act_val.types.iter().all(|av| {
1335 match av {
1336 Atomic::TNever => return true,
1337 Atomic::TClassString(Some(av_cls)) => {
1338 return dec_val.types.iter().any(|dv| match dv {
1339 Atomic::TClassString(None) | Atomic::TString => true,
1340 Atomic::TClassString(Some(dv_cls)) => {
1341 av_cls == dv_cls
1342 || codebase
1343 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1344 }
1345 _ => false,
1346 });
1347 }
1348 _ => {}
1349 }
1350 let av_fqcn: &Arc<str> = match av {
1351 Atomic::TNamedObject { fqcn, .. } => fqcn,
1352 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1353 Atomic::TClosure { .. } => return true,
1354 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1355 };
1356 dec_val.types.iter().any(|dv| {
1357 let dv_fqcn: &Arc<str> = match dv {
1358 Atomic::TNamedObject { fqcn, .. } => fqcn,
1359 Atomic::TClosure { .. } => return true,
1360 _ => return false,
1361 };
1362 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1363 return true; }
1365 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1366 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1367 res_dec == res_act
1368 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1369 || codebase.extends_or_implements(&res_act, &res_dec)
1370 })
1371 })
1372 })
1373 })
1374}