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_ast::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_ast::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) = {
133 let lc = self.source_map.offset_to_line_col(stmt.span.start);
134 (lc.line + 1, lc.col as u16)
135 };
136 let mut issue = mir_issues::Issue::new(
137 IssueKind::TaintedHtml,
138 mir_issues::Location {
139 file: self.file.clone(),
140 line,
141 col_start: col,
142 col_end: col,
143 },
144 );
145 let start = stmt.span.start as usize;
147 let end = stmt.span.end as usize;
148 if start < self.source.len() {
149 let end = end.min(self.source.len());
150 let span_text = &self.source[start..end];
151 if let Some(first_line) = span_text.lines().next() {
152 issue = issue.with_snippet(first_line.trim().to_string());
153 }
154 }
155 self.issues.add(issue);
156 }
157 self.expr_analyzer(ctx).analyze(expr, ctx);
158 }
159 }
160
161 StmtKind::Return(opt_expr) => {
163 if let Some(expr) = opt_expr {
164 let ret_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
165
166 let check_ty =
171 if let Some((None, var_ty)) = self.extract_var_annotation(stmt.span) {
172 var_ty
173 } else {
174 ret_ty.clone()
175 };
176
177 if let Some(declared) = &ctx.fn_return_type.clone() {
179 if (declared.is_void() && !check_ty.is_void() && !check_ty.is_mixed())
183 || (!check_ty.is_subtype_of_simple(declared)
184 && !declared.is_mixed()
185 && !check_ty.is_mixed()
186 && !named_object_return_compatible(&check_ty, declared, self.codebase, &self.file)
187 && (check_ty.remove_null().is_empty() || !named_object_return_compatible(&check_ty.remove_null(), declared, self.codebase, &self.file))
191 && !declared_return_has_template(declared, self.codebase)
192 && !declared_return_has_template(&check_ty, self.codebase)
193 && !return_arrays_compatible(&check_ty, declared, self.codebase, &self.file)
194 && !declared.is_subtype_of_simple(&check_ty)
196 && !declared.remove_null().is_subtype_of_simple(&check_ty)
197 && (check_ty.remove_null().is_empty() || !check_ty.remove_null().is_subtype_of_simple(declared))
202 && !check_ty.remove_false().is_subtype_of_simple(declared)
203 && !named_object_return_compatible(declared, &check_ty, self.codebase, &self.file)
206 && !named_object_return_compatible(&declared.remove_null(), &check_ty.remove_null(), self.codebase, &self.file))
207 {
208 let (line, col) = {
209 let lc = self.source_map.offset_to_line_col(stmt.span.start);
210 (lc.line + 1, lc.col as u16)
211 };
212 self.issues.add(
213 mir_issues::Issue::new(
214 IssueKind::InvalidReturnType {
215 expected: format!("{}", declared),
216 actual: format!("{}", ret_ty),
217 },
218 mir_issues::Location {
219 file: self.file.clone(),
220 line,
221 col_start: col,
222 col_end: col,
223 },
224 )
225 .with_snippet(
226 crate::parser::span_text(self.source, stmt.span)
227 .unwrap_or_default(),
228 ),
229 );
230 }
231 }
232 self.return_types.push(ret_ty);
233 } else {
234 self.return_types.push(Union::single(Atomic::TVoid));
235 if let Some(declared) = &ctx.fn_return_type.clone() {
237 if !declared.is_void() && !declared.is_mixed() {
238 let (line, col) = {
239 let lc = self.source_map.offset_to_line_col(stmt.span.start);
240 (lc.line + 1, lc.col as u16)
241 };
242 self.issues.add(
243 mir_issues::Issue::new(
244 IssueKind::InvalidReturnType {
245 expected: format!("{}", declared),
246 actual: "void".to_string(),
247 },
248 mir_issues::Location {
249 file: self.file.clone(),
250 line,
251 col_start: col,
252 col_end: col,
253 },
254 )
255 .with_snippet(
256 crate::parser::span_text(self.source, stmt.span)
257 .unwrap_or_default(),
258 ),
259 );
260 }
261 }
262 }
263 ctx.diverges = true;
264 }
265
266 StmtKind::Throw(expr) => {
268 let thrown_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
269 for atomic in &thrown_ty.types {
271 match atomic {
272 mir_types::Atomic::TNamedObject { fqcn, .. } => {
273 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
274 let is_throwable = resolved == "Throwable"
275 || resolved == "Exception"
276 || resolved == "Error"
277 || fqcn.as_ref() == "Throwable"
278 || fqcn.as_ref() == "Exception"
279 || fqcn.as_ref() == "Error"
280 || self.codebase.extends_or_implements(&resolved, "Throwable")
281 || self.codebase.extends_or_implements(&resolved, "Exception")
282 || self.codebase.extends_or_implements(&resolved, "Error")
283 || self.codebase.extends_or_implements(fqcn, "Throwable")
284 || self.codebase.extends_or_implements(fqcn, "Exception")
285 || self.codebase.extends_or_implements(fqcn, "Error")
286 || self.codebase.has_unknown_ancestor(&resolved)
288 || self.codebase.has_unknown_ancestor(fqcn)
289 || (!self.codebase.type_exists(&resolved) && !self.codebase.type_exists(fqcn));
291 if !is_throwable {
292 let (line, col) = {
293 let lc = self.source_map.offset_to_line_col(stmt.span.start);
294 (lc.line + 1, lc.col as u16)
295 };
296 self.issues.add(mir_issues::Issue::new(
297 IssueKind::InvalidThrow {
298 ty: fqcn.to_string(),
299 },
300 mir_issues::Location {
301 file: self.file.clone(),
302 line,
303 col_start: col,
304 col_end: col,
305 },
306 ));
307 }
308 }
309 mir_types::Atomic::TSelf { fqcn }
311 | mir_types::Atomic::TStaticObject { fqcn }
312 | mir_types::Atomic::TParent { fqcn } => {
313 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
314 let is_throwable = resolved == "Throwable"
315 || resolved == "Exception"
316 || resolved == "Error"
317 || self.codebase.extends_or_implements(&resolved, "Throwable")
318 || self.codebase.extends_or_implements(&resolved, "Exception")
319 || self.codebase.extends_or_implements(&resolved, "Error")
320 || self.codebase.extends_or_implements(fqcn, "Throwable")
321 || self.codebase.extends_or_implements(fqcn, "Exception")
322 || self.codebase.extends_or_implements(fqcn, "Error")
323 || self.codebase.has_unknown_ancestor(&resolved)
324 || self.codebase.has_unknown_ancestor(fqcn);
325 if !is_throwable {
326 let (line, col) = {
327 let lc = self.source_map.offset_to_line_col(stmt.span.start);
328 (lc.line + 1, lc.col as u16)
329 };
330 self.issues.add(mir_issues::Issue::new(
331 IssueKind::InvalidThrow {
332 ty: fqcn.to_string(),
333 },
334 mir_issues::Location {
335 file: self.file.clone(),
336 line,
337 col_start: col,
338 col_end: col,
339 },
340 ));
341 }
342 }
343 mir_types::Atomic::TMixed | mir_types::Atomic::TObject => {}
344 _ => {
345 let (line, col) = {
346 let lc = self.source_map.offset_to_line_col(stmt.span.start);
347 (lc.line + 1, lc.col as u16)
348 };
349 self.issues.add(mir_issues::Issue::new(
350 IssueKind::InvalidThrow {
351 ty: format!("{}", thrown_ty),
352 },
353 mir_issues::Location {
354 file: self.file.clone(),
355 line,
356 col_start: col,
357 col_end: col,
358 },
359 ));
360 }
361 }
362 }
363 ctx.diverges = true;
364 }
365
366 StmtKind::If(if_stmt) => {
368 let pre_ctx = ctx.clone();
369
370 let cond_type = self.expr_analyzer(ctx).analyze(&if_stmt.condition, ctx);
372 let pre_diverges = ctx.diverges;
373
374 let mut then_ctx = ctx.fork();
376 narrow_from_condition(
377 &if_stmt.condition,
378 &mut then_ctx,
379 true,
380 self.codebase,
381 &self.file,
382 );
383 if !then_ctx.diverges {
386 self.analyze_stmt(if_stmt.then_branch, &mut then_ctx);
387 }
388
389 let mut elseif_ctxs: Vec<Context> = vec![];
391 for elseif in if_stmt.elseif_branches.iter() {
392 let mut branch_ctx = ctx.fork();
393 narrow_from_condition(
394 &elseif.condition,
395 &mut branch_ctx,
396 true,
397 self.codebase,
398 &self.file,
399 );
400 self.expr_analyzer(&branch_ctx)
401 .analyze(&elseif.condition, &mut branch_ctx);
402 if !branch_ctx.diverges {
403 self.analyze_stmt(&elseif.body, &mut branch_ctx);
404 }
405 elseif_ctxs.push(branch_ctx);
406 }
407
408 let mut else_ctx = ctx.fork();
410 narrow_from_condition(
411 &if_stmt.condition,
412 &mut else_ctx,
413 false,
414 self.codebase,
415 &self.file,
416 );
417 if !else_ctx.diverges {
418 if let Some(else_branch) = &if_stmt.else_branch {
419 self.analyze_stmt(else_branch, &mut else_ctx);
420 }
421 }
422
423 if !pre_diverges && (then_ctx.diverges || else_ctx.diverges) {
425 let lc = self
426 .source_map
427 .offset_to_line_col(if_stmt.condition.span.start);
428 let (line, col) = (lc.line + 1, lc.col as u16);
429 self.issues.add(
430 mir_issues::Issue::new(
431 IssueKind::RedundantCondition {
432 ty: format!("{}", cond_type),
433 },
434 mir_issues::Location {
435 file: self.file.clone(),
436 line,
437 col_start: col,
438 col_end: col,
439 },
440 )
441 .with_snippet(
442 crate::parser::span_text(self.source, if_stmt.condition.span)
443 .unwrap_or_default(),
444 ),
445 );
446 }
447
448 *ctx = Context::merge_branches(&pre_ctx, then_ctx, Some(else_ctx));
450 for ec in elseif_ctxs {
451 *ctx = Context::merge_branches(&pre_ctx, ec, None);
452 }
453 }
454
455 StmtKind::While(w) => {
457 self.expr_analyzer(ctx).analyze(&w.condition, ctx);
458 let pre = ctx.clone();
459
460 let mut entry = ctx.fork();
462 narrow_from_condition(&w.condition, &mut entry, true, self.codebase, &self.file);
463
464 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
465 sa.analyze_stmt(w.body, iter);
466 sa.expr_analyzer(iter).analyze(&w.condition, iter);
467 });
468 *ctx = post;
469 }
470
471 StmtKind::DoWhile(dw) => {
473 let pre = ctx.clone();
474 let entry = ctx.fork();
475 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
476 sa.analyze_stmt(dw.body, iter);
477 sa.expr_analyzer(iter).analyze(&dw.condition, iter);
478 });
479 *ctx = post;
480 }
481
482 StmtKind::For(f) => {
484 for init in f.init.iter() {
486 self.expr_analyzer(ctx).analyze(init, ctx);
487 }
488 let pre = ctx.clone();
489 let mut entry = ctx.fork();
490 for cond in f.condition.iter() {
491 self.expr_analyzer(&entry).analyze(cond, &mut entry);
492 }
493
494 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
495 sa.analyze_stmt(f.body, iter);
496 for update in f.update.iter() {
497 sa.expr_analyzer(iter).analyze(update, iter);
498 }
499 for cond in f.condition.iter() {
500 sa.expr_analyzer(iter).analyze(cond, iter);
501 }
502 });
503 *ctx = post;
504 }
505
506 StmtKind::Foreach(fe) => {
508 let arr_ty = self.expr_analyzer(ctx).analyze(&fe.expr, ctx);
509 let (key_ty, mut value_ty) = infer_foreach_types(&arr_ty);
510
511 if let Some(vname) = crate::expr::extract_simple_var(&fe.value) {
514 if let Some((Some(ann_var), ann_ty)) = self.extract_var_annotation(stmt.span) {
515 if ann_var == vname {
516 value_ty = ann_ty;
517 }
518 }
519 }
520
521 let pre = ctx.clone();
522 let mut entry = ctx.fork();
523
524 if let Some(key_expr) = &fe.key {
526 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
527 entry.set_var(var_name, key_ty.clone());
528 }
529 }
530 let value_var = crate::expr::extract_simple_var(&fe.value);
533 let value_destructure_vars = crate::expr::extract_destructure_vars(&fe.value);
534 if let Some(ref vname) = value_var {
535 entry.set_var(vname.as_str(), value_ty.clone());
536 } else {
537 for vname in &value_destructure_vars {
538 entry.set_var(vname, Union::mixed());
539 }
540 }
541
542 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
543 if let Some(key_expr) = &fe.key {
545 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
546 iter.set_var(var_name, key_ty.clone());
547 }
548 }
549 if let Some(ref vname) = value_var {
550 iter.set_var(vname.as_str(), value_ty.clone());
551 } else {
552 for vname in &value_destructure_vars {
553 iter.set_var(vname, Union::mixed());
554 }
555 }
556 sa.analyze_stmt(fe.body, iter);
557 });
558 *ctx = post;
559 }
560
561 StmtKind::Switch(sw) => {
563 let _subject_ty = self.expr_analyzer(ctx).analyze(&sw.expr, ctx);
564 let subject_var: Option<String> = match &sw.expr.kind {
566 php_ast::ast::ExprKind::Variable(name) => {
567 Some(name.as_ref().trim_start_matches('$').to_string())
568 }
569 _ => None,
570 };
571 let switch_on_true = matches!(&sw.expr.kind, php_ast::ast::ExprKind::Bool(true));
573
574 let pre_ctx = ctx.clone();
575 self.break_ctx_stack.push(Vec::new());
578
579 let mut all_cases_diverge = true;
580 let has_default = sw.cases.iter().any(|c| c.value.is_none());
581
582 for case in sw.cases.iter() {
583 let mut case_ctx = pre_ctx.fork();
584 if let Some(val) = &case.value {
585 if switch_on_true {
586 narrow_from_condition(
588 val,
589 &mut case_ctx,
590 true,
591 self.codebase,
592 &self.file,
593 );
594 } else if let Some(ref var_name) = subject_var {
595 let narrow_ty = match &val.kind {
597 php_ast::ast::ExprKind::Int(n) => {
598 Some(Union::single(Atomic::TLiteralInt(*n)))
599 }
600 php_ast::ast::ExprKind::String(s) => {
601 Some(Union::single(Atomic::TLiteralString(Arc::from(&**s))))
602 }
603 php_ast::ast::ExprKind::Bool(b) => Some(Union::single(if *b {
604 Atomic::TTrue
605 } else {
606 Atomic::TFalse
607 })),
608 php_ast::ast::ExprKind::Null => Some(Union::single(Atomic::TNull)),
609 _ => None,
610 };
611 if let Some(narrowed) = narrow_ty {
612 case_ctx.set_var(var_name, narrowed);
613 }
614 }
615 self.expr_analyzer(&case_ctx).analyze(val, &mut case_ctx);
616 }
617 for stmt in case.body.iter() {
618 self.analyze_stmt(stmt, &mut case_ctx);
619 }
620 if !case_ctx.diverges {
621 all_cases_diverge = false;
622 }
623 }
624
625 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
628
629 let mut merged = if has_default && all_cases_diverge && break_ctxs.is_empty() {
633 let mut m = pre_ctx.clone();
635 m.diverges = true;
636 m
637 } else {
638 pre_ctx.clone()
641 };
642
643 for bctx in break_ctxs {
644 merged = Context::merge_branches(&pre_ctx, bctx, Some(merged));
645 }
646
647 *ctx = merged;
648 }
649
650 StmtKind::TryCatch(tc) => {
652 let pre_ctx = ctx.clone();
653 let mut try_ctx = ctx.fork();
654 for stmt in tc.body.iter() {
655 self.analyze_stmt(stmt, &mut try_ctx);
656 }
657
658 let catch_base = Context::merge_branches(&pre_ctx, try_ctx.clone(), None);
662
663 let mut non_diverging_catches: Vec<Context> = vec![];
664 for catch in tc.catches.iter() {
665 let mut catch_ctx = catch_base.clone();
666 if let Some(var) = catch.var {
667 let exc_ty = if catch.types.is_empty() {
669 Union::single(Atomic::TObject)
670 } else {
671 let mut u = Union::empty();
672 for catch_ty in catch.types.iter() {
673 let raw = crate::parser::name_to_string(catch_ty);
674 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
675 u.add_type(Atomic::TNamedObject {
676 fqcn: resolved.into(),
677 type_params: vec![],
678 });
679 }
680 u
681 };
682 catch_ctx.set_var(var.trim_start_matches('$'), exc_ty);
683 }
684 for stmt in catch.body.iter() {
685 self.analyze_stmt(stmt, &mut catch_ctx);
686 }
687 if !catch_ctx.diverges {
688 non_diverging_catches.push(catch_ctx);
689 }
690 }
691
692 let result = if non_diverging_catches.is_empty() {
696 let mut r = try_ctx;
697 r.diverges = false; r
699 } else {
700 let mut r = try_ctx;
703 for catch_ctx in non_diverging_catches {
704 r = Context::merge_branches(&pre_ctx, r, Some(catch_ctx));
705 }
706 r
707 };
708
709 if let Some(finally_stmts) = &tc.finally {
711 let mut finally_ctx = result.clone();
712 finally_ctx.inside_finally = true;
713 for stmt in finally_stmts.iter() {
714 self.analyze_stmt(stmt, &mut finally_ctx);
715 }
716 }
717
718 *ctx = result;
719 }
720
721 StmtKind::Block(stmts) => {
723 self.analyze_stmts(stmts, ctx);
724 }
725
726 StmtKind::Break(_) => {
728 if let Some(break_ctxs) = self.break_ctx_stack.last_mut() {
731 break_ctxs.push(ctx.clone());
732 }
733 ctx.diverges = true;
736 }
737
738 StmtKind::Continue(_) => {
740 ctx.diverges = true;
743 }
744
745 StmtKind::Unset(vars) => {
747 for var in vars.iter() {
748 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
749 ctx.unset_var(name.as_ref().trim_start_matches('$'));
750 }
751 }
752 }
753
754 StmtKind::StaticVar(vars) => {
756 for sv in vars.iter() {
757 let ty = Union::mixed(); ctx.set_var(sv.name.trim_start_matches('$'), ty);
759 }
760 }
761
762 StmtKind::Global(vars) => {
764 for var in vars.iter() {
765 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
766 ctx.set_var(name.as_ref().trim_start_matches('$'), Union::mixed());
767 }
768 }
769 }
770
771 StmtKind::Declare(d) => {
773 for (name, _val) in d.directives.iter() {
774 if *name == "strict_types" {
775 ctx.strict_types = true;
776 }
777 }
778 if let Some(body) = &d.body {
779 self.analyze_stmt(body, ctx);
780 }
781 }
782
783 StmtKind::Function(_)
785 | StmtKind::Class(_)
786 | StmtKind::Interface(_)
787 | StmtKind::Trait(_)
788 | StmtKind::Enum(_) => {
789 }
791
792 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
794
795 StmtKind::InlineHtml(_)
797 | StmtKind::Nop
798 | StmtKind::Goto(_)
799 | StmtKind::Label(_)
800 | StmtKind::HaltCompiler(_) => {}
801
802 StmtKind::Error => {}
803 }
804 }
805
806 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
811 where
812 'a: 'b,
813 {
814 ExpressionAnalyzer::new(
815 self.codebase,
816 self.file.clone(),
817 self.source,
818 self.source_map,
819 self.issues,
820 self.symbols,
821 )
822 }
823
824 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
831 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
832 return vec![];
833 };
834 let mut suppressions = Vec::new();
835 for line in doc.lines() {
836 let line = line.trim().trim_start_matches('*').trim();
837 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
838 r
839 } else if let Some(r) = line.strip_prefix("@suppress ") {
840 r
841 } else {
842 continue;
843 };
844 for name in rest.split_whitespace() {
845 suppressions.push(name.to_string());
846 }
847 }
848 suppressions
849 }
850
851 fn extract_var_annotation(
855 &self,
856 span: php_ast::Span,
857 ) -> Option<(Option<String>, mir_types::Union)> {
858 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
859 let parsed = crate::parser::DocblockParser::parse(&doc);
860 let ty = parsed.var_type?;
861 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
862 Some((parsed.var_name, resolved))
863 }
864
865 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
880 where
881 F: FnMut(&mut Self, &mut Context),
882 {
883 const MAX_ITERS: usize = 3;
884
885 self.break_ctx_stack.push(Vec::new());
887
888 let mut current = entry;
889 current.inside_loop = true;
890
891 for _ in 0..MAX_ITERS {
892 let prev_vars = current.vars.clone();
893
894 let mut iter = current.clone();
895 body(self, &mut iter);
896
897 let next = Context::merge_branches(pre, iter, None);
898
899 if vars_stabilized(&prev_vars, &next.vars) {
900 current = next;
901 break;
902 }
903 current = next;
904 }
905
906 widen_unstable(&pre.vars, &mut current.vars);
908
909 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
911 for bctx in break_ctxs {
912 current = Context::merge_branches(pre, current, Some(bctx));
913 }
914
915 current
916 }
917}
918
919fn vars_stabilized(
926 prev: &indexmap::IndexMap<String, Union>,
927 next: &indexmap::IndexMap<String, Union>,
928) -> bool {
929 if prev.len() != next.len() {
930 return false;
931 }
932 prev.iter()
933 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
934}
935
936fn widen_unstable(
939 pre_vars: &indexmap::IndexMap<String, Union>,
940 current_vars: &mut indexmap::IndexMap<String, Union>,
941) {
942 for (name, ty) in current_vars.iter_mut() {
943 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
944 *ty = Union::mixed();
945 }
946 }
947}
948
949fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
954 if arr_ty.is_mixed() {
955 return (Union::mixed(), Union::mixed());
956 }
957 for atomic in &arr_ty.types {
958 match atomic {
959 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
960 return (*key.clone(), *value.clone());
961 }
962 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
963 return (Union::single(Atomic::TInt), *value.clone());
964 }
965 Atomic::TKeyedArray { properties, .. } => {
966 let mut values = Union::empty();
967 for (_k, prop) in properties {
968 values = Union::merge(&values, &prop.ty);
969 }
970 let values = if values.is_empty() {
973 Union::mixed()
974 } else {
975 values
976 };
977 return (Union::single(Atomic::TMixed), values);
978 }
979 Atomic::TString => {
980 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
981 }
982 _ => {}
983 }
984 }
985 (Union::mixed(), Union::mixed())
986}
987
988fn named_object_return_compatible(
995 actual: &Union,
996 declared: &Union,
997 codebase: &Codebase,
998 file: &str,
999) -> bool {
1000 actual.types.iter().all(|actual_atom| {
1001 let actual_fqcn: &Arc<str> = match actual_atom {
1003 Atomic::TNamedObject { fqcn, .. } => fqcn,
1004 Atomic::TSelf { fqcn } => fqcn,
1005 Atomic::TStaticObject { fqcn } => fqcn,
1006 Atomic::TParent { fqcn } => fqcn,
1007 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1009 Atomic::TVoid => {
1011 return declared
1012 .types
1013 .iter()
1014 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1015 }
1016 Atomic::TNever => return true,
1018 Atomic::TClassString(Some(actual_cls)) => {
1020 return declared.types.iter().any(|d| match d {
1021 Atomic::TClassString(None) => true,
1022 Atomic::TClassString(Some(declared_cls)) => {
1023 actual_cls == declared_cls
1024 || codebase
1025 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1026 }
1027 Atomic::TString => true,
1028 _ => false,
1029 });
1030 }
1031 Atomic::TClassString(None) => {
1032 return declared
1033 .types
1034 .iter()
1035 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1036 }
1037 _ => return false,
1039 };
1040
1041 declared.types.iter().any(|declared_atom| {
1042 let declared_fqcn: &Arc<str> = match declared_atom {
1044 Atomic::TNamedObject { fqcn, .. } => fqcn,
1045 Atomic::TSelf { fqcn } => fqcn,
1046 Atomic::TStaticObject { fqcn } => fqcn,
1047 Atomic::TParent { fqcn } => fqcn,
1048 _ => return false,
1049 };
1050
1051 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1052 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1053
1054 if matches!(
1056 actual_atom,
1057 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1058 ) && (resolved_actual == resolved_declared
1059 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1060 || actual_fqcn.as_ref() == resolved_declared.as_str()
1061 || resolved_actual.as_str() == declared_fqcn.as_ref()
1062 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1063 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1064 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1065 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1066 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1069 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1070 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1071 {
1072 return true;
1073 }
1074
1075 let is_same_class = resolved_actual == resolved_declared
1077 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1078 || actual_fqcn.as_ref() == resolved_declared.as_str()
1079 || resolved_actual.as_str() == declared_fqcn.as_ref();
1080
1081 if is_same_class {
1082 let actual_type_params = match actual_atom {
1083 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1084 _ => &[],
1085 };
1086 let declared_type_params = match declared_atom {
1087 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1088 _ => &[],
1089 };
1090 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1091 let class_tps = codebase.get_class_template_params(&resolved_declared);
1092 return return_type_params_compatible(
1093 actual_type_params,
1094 declared_type_params,
1095 &class_tps,
1096 );
1097 }
1098 return true;
1099 }
1100
1101 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1103 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1104 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1105 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1106 })
1107 })
1108}
1109
1110fn return_type_params_compatible(
1114 actual_params: &[Union],
1115 declared_params: &[Union],
1116 template_params: &[mir_codebase::storage::TemplateParam],
1117) -> bool {
1118 if actual_params.len() != declared_params.len() {
1119 return true;
1120 }
1121 if actual_params.is_empty() {
1122 return true;
1123 }
1124
1125 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1126 {
1127 let variance = template_params
1128 .get(i)
1129 .map(|tp| tp.variance)
1130 .unwrap_or(mir_types::Variance::Invariant);
1131
1132 let compatible = match variance {
1133 mir_types::Variance::Covariant => {
1134 actual_p.is_subtype_of_simple(declared_p)
1135 || declared_p.is_mixed()
1136 || actual_p.is_mixed()
1137 }
1138 mir_types::Variance::Contravariant => {
1139 declared_p.is_subtype_of_simple(actual_p)
1140 || actual_p.is_mixed()
1141 || declared_p.is_mixed()
1142 }
1143 mir_types::Variance::Invariant => {
1144 actual_p == declared_p
1145 || actual_p.is_mixed()
1146 || declared_p.is_mixed()
1147 || (actual_p.is_subtype_of_simple(declared_p)
1148 && declared_p.is_subtype_of_simple(actual_p))
1149 }
1150 };
1151
1152 if !compatible {
1153 return false;
1154 }
1155 }
1156
1157 true
1158}
1159
1160fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1164 declared.types.iter().any(|atomic| match atomic {
1165 Atomic::TTemplateParam { .. } => true,
1166 Atomic::TNamedObject { fqcn, type_params } => {
1172 !type_params.is_empty()
1173 || !codebase.type_exists(fqcn.as_ref())
1174 || codebase.interfaces.contains_key(fqcn.as_ref())
1175 }
1176 Atomic::TArray { value, .. }
1177 | Atomic::TList { value }
1178 | Atomic::TNonEmptyArray { value, .. }
1179 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1180 Atomic::TTemplateParam { .. } => true,
1181 Atomic::TNamedObject { fqcn, .. } => {
1182 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1183 }
1184 _ => false,
1185 }),
1186 _ => false,
1187 })
1188}
1189
1190fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1193 let mut result = Union::empty();
1194 result.possibly_undefined = union.possibly_undefined;
1195 result.from_docblock = union.from_docblock;
1196 for atomic in union.types {
1197 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1198 result.types.push(resolved);
1199 }
1200 result
1201}
1202
1203fn is_resolvable_class_name(s: &str) -> bool {
1204 !s.is_empty()
1205 && s.chars()
1206 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1207}
1208
1209fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1210 match atomic {
1211 Atomic::TNamedObject { fqcn, type_params } => {
1212 if !is_resolvable_class_name(fqcn.as_ref()) {
1213 return Atomic::TNamedObject { fqcn, type_params };
1214 }
1215 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1216 Atomic::TNamedObject {
1217 fqcn: resolved.into(),
1218 type_params,
1219 }
1220 }
1221 Atomic::TClassString(Some(cls)) => {
1222 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1223 Atomic::TClassString(Some(resolved.into()))
1224 }
1225 Atomic::TList { value } => Atomic::TList {
1226 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1227 },
1228 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1229 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1230 },
1231 Atomic::TArray { key, value } => Atomic::TArray {
1232 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1233 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1234 },
1235 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1236 Atomic::TSelf { fqcn }
1238 }
1239 other => other,
1240 }
1241}
1242
1243fn return_arrays_compatible(
1246 actual: &Union,
1247 declared: &Union,
1248 codebase: &Codebase,
1249 file: &str,
1250) -> bool {
1251 actual.types.iter().all(|a_atomic| {
1252 let act_val: &Union = match a_atomic {
1253 Atomic::TArray { value, .. }
1254 | Atomic::TNonEmptyArray { value, .. }
1255 | Atomic::TList { value }
1256 | Atomic::TNonEmptyList { value } => value,
1257 Atomic::TKeyedArray { .. } => return true,
1258 _ => return false,
1259 };
1260
1261 declared.types.iter().any(|d_atomic| {
1262 let dec_val: &Union = match d_atomic {
1263 Atomic::TArray { value, .. }
1264 | Atomic::TNonEmptyArray { value, .. }
1265 | Atomic::TList { value }
1266 | Atomic::TNonEmptyList { value } => value,
1267 _ => return false,
1268 };
1269
1270 act_val.types.iter().all(|av| {
1271 match av {
1272 Atomic::TNever => return true,
1273 Atomic::TClassString(Some(av_cls)) => {
1274 return dec_val.types.iter().any(|dv| match dv {
1275 Atomic::TClassString(None) | Atomic::TString => true,
1276 Atomic::TClassString(Some(dv_cls)) => {
1277 av_cls == dv_cls
1278 || codebase
1279 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1280 }
1281 _ => false,
1282 });
1283 }
1284 _ => {}
1285 }
1286 let av_fqcn: &Arc<str> = match av {
1287 Atomic::TNamedObject { fqcn, .. } => fqcn,
1288 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1289 Atomic::TClosure { .. } => return true,
1290 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1291 };
1292 dec_val.types.iter().any(|dv| {
1293 let dv_fqcn: &Arc<str> = match dv {
1294 Atomic::TNamedObject { fqcn, .. } => fqcn,
1295 Atomic::TClosure { .. } => return true,
1296 _ => return false,
1297 };
1298 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1299 return true; }
1301 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1302 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1303 res_dec == res_act
1304 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1305 || codebase.extends_or_implements(&res_act, &res_dec)
1306 })
1307 })
1308 })
1309 })
1310}