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) = {
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_str().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_str().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 let var_name = name.as_str().trim_start_matches('$');
767 let ty = self
768 .codebase
769 .global_vars
770 .get(var_name)
771 .map(|r| r.clone())
772 .unwrap_or_else(Union::mixed);
773 ctx.set_var(var_name, ty);
774 }
775 }
776 }
777
778 StmtKind::Declare(d) => {
780 for (name, _val) in d.directives.iter() {
781 if *name == "strict_types" {
782 ctx.strict_types = true;
783 }
784 }
785 if let Some(body) = &d.body {
786 self.analyze_stmt(body, ctx);
787 }
788 }
789
790 StmtKind::Function(_)
792 | StmtKind::Class(_)
793 | StmtKind::Interface(_)
794 | StmtKind::Trait(_)
795 | StmtKind::Enum(_) => {
796 }
798
799 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
801
802 StmtKind::InlineHtml(_)
804 | StmtKind::Nop
805 | StmtKind::Goto(_)
806 | StmtKind::Label(_)
807 | StmtKind::HaltCompiler(_) => {}
808
809 StmtKind::Error => {}
810 }
811 }
812
813 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
818 where
819 'a: 'b,
820 {
821 ExpressionAnalyzer::new(
822 self.codebase,
823 self.file.clone(),
824 self.source,
825 self.source_map,
826 self.issues,
827 self.symbols,
828 )
829 }
830
831 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
838 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
839 return vec![];
840 };
841 let mut suppressions = Vec::new();
842 for line in doc.lines() {
843 let line = line.trim().trim_start_matches('*').trim();
844 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
845 r
846 } else if let Some(r) = line.strip_prefix("@suppress ") {
847 r
848 } else {
849 continue;
850 };
851 for name in rest.split_whitespace() {
852 suppressions.push(name.to_string());
853 }
854 }
855 suppressions
856 }
857
858 fn extract_var_annotation(
862 &self,
863 span: php_ast::Span,
864 ) -> Option<(Option<String>, mir_types::Union)> {
865 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
866 let parsed = crate::parser::DocblockParser::parse(&doc);
867 let ty = parsed.var_type?;
868 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
869 Some((parsed.var_name, resolved))
870 }
871
872 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
887 where
888 F: FnMut(&mut Self, &mut Context),
889 {
890 const MAX_ITERS: usize = 3;
891
892 self.break_ctx_stack.push(Vec::new());
894
895 let mut current = entry;
896 current.inside_loop = true;
897
898 for _ in 0..MAX_ITERS {
899 let prev_vars = current.vars.clone();
900
901 let mut iter = current.clone();
902 body(self, &mut iter);
903
904 let next = Context::merge_branches(pre, iter, None);
905
906 if vars_stabilized(&prev_vars, &next.vars) {
907 current = next;
908 break;
909 }
910 current = next;
911 }
912
913 widen_unstable(&pre.vars, &mut current.vars);
915
916 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
918 for bctx in break_ctxs {
919 current = Context::merge_branches(pre, current, Some(bctx));
920 }
921
922 current
923 }
924}
925
926fn vars_stabilized(
933 prev: &indexmap::IndexMap<String, Union>,
934 next: &indexmap::IndexMap<String, Union>,
935) -> bool {
936 if prev.len() != next.len() {
937 return false;
938 }
939 prev.iter()
940 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
941}
942
943fn widen_unstable(
946 pre_vars: &indexmap::IndexMap<String, Union>,
947 current_vars: &mut indexmap::IndexMap<String, Union>,
948) {
949 for (name, ty) in current_vars.iter_mut() {
950 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
951 *ty = Union::mixed();
952 }
953 }
954}
955
956fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
961 if arr_ty.is_mixed() {
962 return (Union::mixed(), Union::mixed());
963 }
964 for atomic in &arr_ty.types {
965 match atomic {
966 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
967 return (*key.clone(), *value.clone());
968 }
969 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
970 return (Union::single(Atomic::TInt), *value.clone());
971 }
972 Atomic::TKeyedArray { properties, .. } => {
973 let mut values = Union::empty();
974 for (_k, prop) in properties {
975 values = Union::merge(&values, &prop.ty);
976 }
977 let values = if values.is_empty() {
980 Union::mixed()
981 } else {
982 values
983 };
984 return (Union::single(Atomic::TMixed), values);
985 }
986 Atomic::TString => {
987 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
988 }
989 _ => {}
990 }
991 }
992 (Union::mixed(), Union::mixed())
993}
994
995fn named_object_return_compatible(
1002 actual: &Union,
1003 declared: &Union,
1004 codebase: &Codebase,
1005 file: &str,
1006) -> bool {
1007 actual.types.iter().all(|actual_atom| {
1008 let actual_fqcn: &Arc<str> = match actual_atom {
1010 Atomic::TNamedObject { fqcn, .. } => fqcn,
1011 Atomic::TSelf { fqcn } => fqcn,
1012 Atomic::TStaticObject { fqcn } => fqcn,
1013 Atomic::TParent { fqcn } => fqcn,
1014 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1016 Atomic::TVoid => {
1018 return declared
1019 .types
1020 .iter()
1021 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1022 }
1023 Atomic::TNever => return true,
1025 Atomic::TClassString(Some(actual_cls)) => {
1027 return declared.types.iter().any(|d| match d {
1028 Atomic::TClassString(None) => true,
1029 Atomic::TClassString(Some(declared_cls)) => {
1030 actual_cls == declared_cls
1031 || codebase
1032 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1033 }
1034 Atomic::TString => true,
1035 _ => false,
1036 });
1037 }
1038 Atomic::TClassString(None) => {
1039 return declared
1040 .types
1041 .iter()
1042 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1043 }
1044 _ => return false,
1046 };
1047
1048 declared.types.iter().any(|declared_atom| {
1049 let declared_fqcn: &Arc<str> = match declared_atom {
1051 Atomic::TNamedObject { fqcn, .. } => fqcn,
1052 Atomic::TSelf { fqcn } => fqcn,
1053 Atomic::TStaticObject { fqcn } => fqcn,
1054 Atomic::TParent { fqcn } => fqcn,
1055 _ => return false,
1056 };
1057
1058 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1059 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1060
1061 if matches!(
1063 actual_atom,
1064 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1065 ) && (resolved_actual == resolved_declared
1066 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1067 || actual_fqcn.as_ref() == resolved_declared.as_str()
1068 || resolved_actual.as_str() == declared_fqcn.as_ref()
1069 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1070 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1071 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1072 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1073 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1076 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1077 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1078 {
1079 return true;
1080 }
1081
1082 let is_same_class = resolved_actual == resolved_declared
1084 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1085 || actual_fqcn.as_ref() == resolved_declared.as_str()
1086 || resolved_actual.as_str() == declared_fqcn.as_ref();
1087
1088 if is_same_class {
1089 let actual_type_params = match actual_atom {
1090 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1091 _ => &[],
1092 };
1093 let declared_type_params = match declared_atom {
1094 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1095 _ => &[],
1096 };
1097 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1098 let class_tps = codebase.get_class_template_params(&resolved_declared);
1099 return return_type_params_compatible(
1100 actual_type_params,
1101 declared_type_params,
1102 &class_tps,
1103 );
1104 }
1105 return true;
1106 }
1107
1108 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1110 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1111 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1112 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1113 })
1114 })
1115}
1116
1117fn return_type_params_compatible(
1121 actual_params: &[Union],
1122 declared_params: &[Union],
1123 template_params: &[mir_codebase::storage::TemplateParam],
1124) -> bool {
1125 if actual_params.len() != declared_params.len() {
1126 return true;
1127 }
1128 if actual_params.is_empty() {
1129 return true;
1130 }
1131
1132 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1133 {
1134 let variance = template_params
1135 .get(i)
1136 .map(|tp| tp.variance)
1137 .unwrap_or(mir_types::Variance::Invariant);
1138
1139 let compatible = match variance {
1140 mir_types::Variance::Covariant => {
1141 actual_p.is_subtype_of_simple(declared_p)
1142 || declared_p.is_mixed()
1143 || actual_p.is_mixed()
1144 }
1145 mir_types::Variance::Contravariant => {
1146 declared_p.is_subtype_of_simple(actual_p)
1147 || actual_p.is_mixed()
1148 || declared_p.is_mixed()
1149 }
1150 mir_types::Variance::Invariant => {
1151 actual_p == declared_p
1152 || actual_p.is_mixed()
1153 || declared_p.is_mixed()
1154 || (actual_p.is_subtype_of_simple(declared_p)
1155 && declared_p.is_subtype_of_simple(actual_p))
1156 }
1157 };
1158
1159 if !compatible {
1160 return false;
1161 }
1162 }
1163
1164 true
1165}
1166
1167fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1171 declared.types.iter().any(|atomic| match atomic {
1172 Atomic::TTemplateParam { .. } => true,
1173 Atomic::TNamedObject { fqcn, type_params } => {
1179 !type_params.is_empty()
1180 || !codebase.type_exists(fqcn.as_ref())
1181 || codebase.interfaces.contains_key(fqcn.as_ref())
1182 }
1183 Atomic::TArray { value, .. }
1184 | Atomic::TList { value }
1185 | Atomic::TNonEmptyArray { value, .. }
1186 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1187 Atomic::TTemplateParam { .. } => true,
1188 Atomic::TNamedObject { fqcn, .. } => {
1189 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1190 }
1191 _ => false,
1192 }),
1193 _ => false,
1194 })
1195}
1196
1197fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1200 let mut result = Union::empty();
1201 result.possibly_undefined = union.possibly_undefined;
1202 result.from_docblock = union.from_docblock;
1203 for atomic in union.types {
1204 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1205 result.types.push(resolved);
1206 }
1207 result
1208}
1209
1210fn is_resolvable_class_name(s: &str) -> bool {
1211 !s.is_empty()
1212 && s.chars()
1213 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1214}
1215
1216fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1217 match atomic {
1218 Atomic::TNamedObject { fqcn, type_params } => {
1219 if !is_resolvable_class_name(fqcn.as_ref()) {
1220 return Atomic::TNamedObject { fqcn, type_params };
1221 }
1222 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1223 Atomic::TNamedObject {
1224 fqcn: resolved.into(),
1225 type_params,
1226 }
1227 }
1228 Atomic::TClassString(Some(cls)) => {
1229 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1230 Atomic::TClassString(Some(resolved.into()))
1231 }
1232 Atomic::TList { value } => Atomic::TList {
1233 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1234 },
1235 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1236 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1237 },
1238 Atomic::TArray { key, value } => Atomic::TArray {
1239 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1240 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1241 },
1242 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1243 Atomic::TSelf { fqcn }
1245 }
1246 other => other,
1247 }
1248}
1249
1250fn return_arrays_compatible(
1253 actual: &Union,
1254 declared: &Union,
1255 codebase: &Codebase,
1256 file: &str,
1257) -> bool {
1258 actual.types.iter().all(|a_atomic| {
1259 let act_val: &Union = match a_atomic {
1260 Atomic::TArray { value, .. }
1261 | Atomic::TNonEmptyArray { value, .. }
1262 | Atomic::TList { value }
1263 | Atomic::TNonEmptyList { value } => value,
1264 Atomic::TKeyedArray { .. } => return true,
1265 _ => return false,
1266 };
1267
1268 declared.types.iter().any(|d_atomic| {
1269 let dec_val: &Union = match d_atomic {
1270 Atomic::TArray { value, .. }
1271 | Atomic::TNonEmptyArray { value, .. }
1272 | Atomic::TList { value }
1273 | Atomic::TNonEmptyList { value } => value,
1274 _ => return false,
1275 };
1276
1277 act_val.types.iter().all(|av| {
1278 match av {
1279 Atomic::TNever => return true,
1280 Atomic::TClassString(Some(av_cls)) => {
1281 return dec_val.types.iter().any(|dv| match dv {
1282 Atomic::TClassString(None) | Atomic::TString => true,
1283 Atomic::TClassString(Some(dv_cls)) => {
1284 av_cls == dv_cls
1285 || codebase
1286 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1287 }
1288 _ => false,
1289 });
1290 }
1291 _ => {}
1292 }
1293 let av_fqcn: &Arc<str> = match av {
1294 Atomic::TNamedObject { fqcn, .. } => fqcn,
1295 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1296 Atomic::TClosure { .. } => return true,
1297 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1298 };
1299 dec_val.types.iter().any(|dv| {
1300 let dv_fqcn: &Arc<str> = match dv {
1301 Atomic::TNamedObject { fqcn, .. } => fqcn,
1302 Atomic::TClosure { .. } => return true,
1303 _ => return false,
1304 };
1305 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1306 return true; }
1308 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1309 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1310 res_dec == res_act
1311 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1312 || codebase.extends_or_implements(&res_act, &res_dec)
1313 })
1314 })
1315 })
1316 })
1317}