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(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(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(stmt.span.start);
212 let col_end = if stmt.span.start < stmt.span.end {
213 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
214 end_col
215 } else {
216 col_start
217 };
218 self.issues.add(
219 mir_issues::Issue::new(
220 IssueKind::InvalidReturnType {
221 expected: format!("{}", declared),
222 actual: format!("{}", ret_ty),
223 },
224 mir_issues::Location {
225 file: self.file.clone(),
226 line,
227 col_start,
228 col_end: col_end.max(col_start + 1),
229 },
230 )
231 .with_snippet(
232 crate::parser::span_text(self.source, stmt.span)
233 .unwrap_or_default(),
234 ),
235 );
236 }
237 }
238 self.return_types.push(ret_ty);
239 } else {
240 self.return_types.push(Union::single(Atomic::TVoid));
241 if let Some(declared) = &ctx.fn_return_type.clone() {
243 if !declared.is_void() && !declared.is_mixed() {
244 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
245 let col_end = if stmt.span.start < stmt.span.end {
246 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
247 end_col
248 } else {
249 col_start
250 };
251 self.issues.add(
252 mir_issues::Issue::new(
253 IssueKind::InvalidReturnType {
254 expected: format!("{}", declared),
255 actual: "void".to_string(),
256 },
257 mir_issues::Location {
258 file: self.file.clone(),
259 line,
260 col_start,
261 col_end: col_end.max(col_start + 1),
262 },
263 )
264 .with_snippet(
265 crate::parser::span_text(self.source, stmt.span)
266 .unwrap_or_default(),
267 ),
268 );
269 }
270 }
271 }
272 ctx.diverges = true;
273 }
274
275 StmtKind::Throw(expr) => {
277 let thrown_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
278 for atomic in &thrown_ty.types {
280 match atomic {
281 mir_types::Atomic::TNamedObject { fqcn, .. } => {
282 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
283 let is_throwable = resolved == "Throwable"
284 || resolved == "Exception"
285 || resolved == "Error"
286 || fqcn.as_ref() == "Throwable"
287 || fqcn.as_ref() == "Exception"
288 || fqcn.as_ref() == "Error"
289 || self.codebase.extends_or_implements(&resolved, "Throwable")
290 || self.codebase.extends_or_implements(&resolved, "Exception")
291 || self.codebase.extends_or_implements(&resolved, "Error")
292 || self.codebase.extends_or_implements(fqcn, "Throwable")
293 || self.codebase.extends_or_implements(fqcn, "Exception")
294 || self.codebase.extends_or_implements(fqcn, "Error")
295 || self.codebase.has_unknown_ancestor(&resolved)
297 || self.codebase.has_unknown_ancestor(fqcn)
298 || (!self.codebase.type_exists(&resolved) && !self.codebase.type_exists(fqcn));
300 if !is_throwable {
301 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
302 let col_end = if stmt.span.start < stmt.span.end {
303 let (_end_line, end_col) =
304 self.offset_to_line_col(stmt.span.end);
305 end_col
306 } else {
307 col_start
308 };
309 self.issues.add(mir_issues::Issue::new(
310 IssueKind::InvalidThrow {
311 ty: fqcn.to_string(),
312 },
313 mir_issues::Location {
314 file: self.file.clone(),
315 line,
316 col_start,
317 col_end: col_end.max(col_start + 1),
318 },
319 ));
320 }
321 }
322 mir_types::Atomic::TSelf { fqcn }
324 | mir_types::Atomic::TStaticObject { fqcn }
325 | mir_types::Atomic::TParent { fqcn } => {
326 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
327 let is_throwable = resolved == "Throwable"
328 || resolved == "Exception"
329 || resolved == "Error"
330 || self.codebase.extends_or_implements(&resolved, "Throwable")
331 || self.codebase.extends_or_implements(&resolved, "Exception")
332 || self.codebase.extends_or_implements(&resolved, "Error")
333 || self.codebase.extends_or_implements(fqcn, "Throwable")
334 || self.codebase.extends_or_implements(fqcn, "Exception")
335 || self.codebase.extends_or_implements(fqcn, "Error")
336 || self.codebase.has_unknown_ancestor(&resolved)
337 || self.codebase.has_unknown_ancestor(fqcn);
338 if !is_throwable {
339 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
340 let col_end = if stmt.span.start < stmt.span.end {
341 let (_end_line, end_col) =
342 self.offset_to_line_col(stmt.span.end);
343 end_col
344 } else {
345 col_start
346 };
347 self.issues.add(mir_issues::Issue::new(
348 IssueKind::InvalidThrow {
349 ty: fqcn.to_string(),
350 },
351 mir_issues::Location {
352 file: self.file.clone(),
353 line,
354 col_start,
355 col_end: col_end.max(col_start + 1),
356 },
357 ));
358 }
359 }
360 mir_types::Atomic::TMixed | mir_types::Atomic::TObject => {}
361 _ => {
362 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
363 let col_end = if stmt.span.start < stmt.span.end {
364 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
365 end_col
366 } else {
367 col_start
368 };
369 self.issues.add(mir_issues::Issue::new(
370 IssueKind::InvalidThrow {
371 ty: format!("{}", thrown_ty),
372 },
373 mir_issues::Location {
374 file: self.file.clone(),
375 line,
376 col_start,
377 col_end: col_end.max(col_start + 1),
378 },
379 ));
380 }
381 }
382 }
383 ctx.diverges = true;
384 }
385
386 StmtKind::If(if_stmt) => {
388 let pre_ctx = ctx.clone();
389
390 let cond_type = self.expr_analyzer(ctx).analyze(&if_stmt.condition, ctx);
392 let pre_diverges = ctx.diverges;
393
394 let mut then_ctx = ctx.fork();
396 narrow_from_condition(
397 &if_stmt.condition,
398 &mut then_ctx,
399 true,
400 self.codebase,
401 &self.file,
402 );
403 if !then_ctx.diverges {
406 self.analyze_stmt(if_stmt.then_branch, &mut then_ctx);
407 }
408
409 let mut elseif_ctxs: Vec<Context> = vec![];
411 for elseif in if_stmt.elseif_branches.iter() {
412 let mut branch_ctx = ctx.fork();
413 narrow_from_condition(
414 &elseif.condition,
415 &mut branch_ctx,
416 true,
417 self.codebase,
418 &self.file,
419 );
420 self.expr_analyzer(&branch_ctx)
421 .analyze(&elseif.condition, &mut branch_ctx);
422 if !branch_ctx.diverges {
423 self.analyze_stmt(&elseif.body, &mut branch_ctx);
424 }
425 elseif_ctxs.push(branch_ctx);
426 }
427
428 let mut else_ctx = ctx.fork();
430 narrow_from_condition(
431 &if_stmt.condition,
432 &mut else_ctx,
433 false,
434 self.codebase,
435 &self.file,
436 );
437 if !else_ctx.diverges {
438 if let Some(else_branch) = &if_stmt.else_branch {
439 self.analyze_stmt(else_branch, &mut else_ctx);
440 }
441 }
442
443 if !pre_diverges && (then_ctx.diverges || else_ctx.diverges) {
445 let (line, col_start) = self.offset_to_line_col(if_stmt.condition.span.start);
446 let col_end = if if_stmt.condition.span.start < if_stmt.condition.span.end {
447 let (_end_line, end_col) =
448 self.offset_to_line_col(if_stmt.condition.span.end);
449 end_col
450 } else {
451 col_start
452 };
453 self.issues.add(
454 mir_issues::Issue::new(
455 IssueKind::RedundantCondition {
456 ty: format!("{}", cond_type),
457 },
458 mir_issues::Location {
459 file: self.file.clone(),
460 line,
461 col_start,
462 col_end: col_end.max(col_start + 1),
463 },
464 )
465 .with_snippet(
466 crate::parser::span_text(self.source, if_stmt.condition.span)
467 .unwrap_or_default(),
468 ),
469 );
470 }
471
472 *ctx = Context::merge_branches(&pre_ctx, then_ctx, Some(else_ctx));
474 for ec in elseif_ctxs {
475 *ctx = Context::merge_branches(&pre_ctx, ec, None);
476 }
477 }
478
479 StmtKind::While(w) => {
481 self.expr_analyzer(ctx).analyze(&w.condition, ctx);
482 let pre = ctx.clone();
483
484 let mut entry = ctx.fork();
486 narrow_from_condition(&w.condition, &mut entry, true, self.codebase, &self.file);
487
488 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
489 sa.analyze_stmt(w.body, iter);
490 sa.expr_analyzer(iter).analyze(&w.condition, iter);
491 });
492 *ctx = post;
493 }
494
495 StmtKind::DoWhile(dw) => {
497 let pre = ctx.clone();
498 let entry = ctx.fork();
499 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
500 sa.analyze_stmt(dw.body, iter);
501 sa.expr_analyzer(iter).analyze(&dw.condition, iter);
502 });
503 *ctx = post;
504 }
505
506 StmtKind::For(f) => {
508 for init in f.init.iter() {
510 self.expr_analyzer(ctx).analyze(init, ctx);
511 }
512 let pre = ctx.clone();
513 let mut entry = ctx.fork();
514 for cond in f.condition.iter() {
515 self.expr_analyzer(&entry).analyze(cond, &mut entry);
516 }
517
518 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
519 sa.analyze_stmt(f.body, iter);
520 for update in f.update.iter() {
521 sa.expr_analyzer(iter).analyze(update, iter);
522 }
523 for cond in f.condition.iter() {
524 sa.expr_analyzer(iter).analyze(cond, iter);
525 }
526 });
527 *ctx = post;
528 }
529
530 StmtKind::Foreach(fe) => {
532 let arr_ty = self.expr_analyzer(ctx).analyze(&fe.expr, ctx);
533 let (key_ty, mut value_ty) = infer_foreach_types(&arr_ty);
534
535 if let Some(vname) = crate::expr::extract_simple_var(&fe.value) {
538 if let Some((Some(ann_var), ann_ty)) = self.extract_var_annotation(stmt.span) {
539 if ann_var == vname {
540 value_ty = ann_ty;
541 }
542 }
543 }
544
545 let pre = ctx.clone();
546 let mut entry = ctx.fork();
547
548 if let Some(key_expr) = &fe.key {
550 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
551 entry.set_var(var_name, key_ty.clone());
552 }
553 }
554 let value_var = crate::expr::extract_simple_var(&fe.value);
557 let value_destructure_vars = crate::expr::extract_destructure_vars(&fe.value);
558 if let Some(ref vname) = value_var {
559 entry.set_var(vname.as_str(), value_ty.clone());
560 } else {
561 for vname in &value_destructure_vars {
562 entry.set_var(vname, Union::mixed());
563 }
564 }
565
566 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
567 if let Some(key_expr) = &fe.key {
569 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
570 iter.set_var(var_name, key_ty.clone());
571 }
572 }
573 if let Some(ref vname) = value_var {
574 iter.set_var(vname.as_str(), value_ty.clone());
575 } else {
576 for vname in &value_destructure_vars {
577 iter.set_var(vname, Union::mixed());
578 }
579 }
580 sa.analyze_stmt(fe.body, iter);
581 });
582 *ctx = post;
583 }
584
585 StmtKind::Switch(sw) => {
587 let _subject_ty = self.expr_analyzer(ctx).analyze(&sw.expr, ctx);
588 let subject_var: Option<String> = match &sw.expr.kind {
590 php_ast::ast::ExprKind::Variable(name) => {
591 Some(name.as_str().trim_start_matches('$').to_string())
592 }
593 _ => None,
594 };
595 let switch_on_true = matches!(&sw.expr.kind, php_ast::ast::ExprKind::Bool(true));
597
598 let pre_ctx = ctx.clone();
599 self.break_ctx_stack.push(Vec::new());
602
603 let mut all_cases_diverge = true;
604 let has_default = sw.cases.iter().any(|c| c.value.is_none());
605
606 for case in sw.cases.iter() {
607 let mut case_ctx = pre_ctx.fork();
608 if let Some(val) = &case.value {
609 if switch_on_true {
610 narrow_from_condition(
612 val,
613 &mut case_ctx,
614 true,
615 self.codebase,
616 &self.file,
617 );
618 } else if let Some(ref var_name) = subject_var {
619 let narrow_ty = match &val.kind {
621 php_ast::ast::ExprKind::Int(n) => {
622 Some(Union::single(Atomic::TLiteralInt(*n)))
623 }
624 php_ast::ast::ExprKind::String(s) => {
625 Some(Union::single(Atomic::TLiteralString(Arc::from(&**s))))
626 }
627 php_ast::ast::ExprKind::Bool(b) => Some(Union::single(if *b {
628 Atomic::TTrue
629 } else {
630 Atomic::TFalse
631 })),
632 php_ast::ast::ExprKind::Null => Some(Union::single(Atomic::TNull)),
633 _ => None,
634 };
635 if let Some(narrowed) = narrow_ty {
636 case_ctx.set_var(var_name, narrowed);
637 }
638 }
639 self.expr_analyzer(&case_ctx).analyze(val, &mut case_ctx);
640 }
641 for stmt in case.body.iter() {
642 self.analyze_stmt(stmt, &mut case_ctx);
643 }
644 if !case_ctx.diverges {
645 all_cases_diverge = false;
646 }
647 }
648
649 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
652
653 let mut merged = if has_default && all_cases_diverge && break_ctxs.is_empty() {
657 let mut m = pre_ctx.clone();
659 m.diverges = true;
660 m
661 } else {
662 pre_ctx.clone()
665 };
666
667 for bctx in break_ctxs {
668 merged = Context::merge_branches(&pre_ctx, bctx, Some(merged));
669 }
670
671 *ctx = merged;
672 }
673
674 StmtKind::TryCatch(tc) => {
676 let pre_ctx = ctx.clone();
677 let mut try_ctx = ctx.fork();
678 for stmt in tc.body.iter() {
679 self.analyze_stmt(stmt, &mut try_ctx);
680 }
681
682 let catch_base = Context::merge_branches(&pre_ctx, try_ctx.clone(), None);
686
687 let mut non_diverging_catches: Vec<Context> = vec![];
688 for catch in tc.catches.iter() {
689 let mut catch_ctx = catch_base.clone();
690 if let Some(var) = catch.var {
691 let exc_ty = if catch.types.is_empty() {
693 Union::single(Atomic::TObject)
694 } else {
695 let mut u = Union::empty();
696 for catch_ty in catch.types.iter() {
697 let raw = crate::parser::name_to_string(catch_ty);
698 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
699 u.add_type(Atomic::TNamedObject {
700 fqcn: resolved.into(),
701 type_params: vec![],
702 });
703 }
704 u
705 };
706 catch_ctx.set_var(var.trim_start_matches('$'), exc_ty);
707 }
708 for stmt in catch.body.iter() {
709 self.analyze_stmt(stmt, &mut catch_ctx);
710 }
711 if !catch_ctx.diverges {
712 non_diverging_catches.push(catch_ctx);
713 }
714 }
715
716 let result = if non_diverging_catches.is_empty() {
720 let mut r = try_ctx;
721 r.diverges = false; r
723 } else {
724 let mut r = try_ctx;
727 for catch_ctx in non_diverging_catches {
728 r = Context::merge_branches(&pre_ctx, r, Some(catch_ctx));
729 }
730 r
731 };
732
733 if let Some(finally_stmts) = &tc.finally {
735 let mut finally_ctx = result.clone();
736 finally_ctx.inside_finally = true;
737 for stmt in finally_stmts.iter() {
738 self.analyze_stmt(stmt, &mut finally_ctx);
739 }
740 }
741
742 *ctx = result;
743 }
744
745 StmtKind::Block(stmts) => {
747 self.analyze_stmts(stmts, ctx);
748 }
749
750 StmtKind::Break(_) => {
752 if let Some(break_ctxs) = self.break_ctx_stack.last_mut() {
755 break_ctxs.push(ctx.clone());
756 }
757 ctx.diverges = true;
760 }
761
762 StmtKind::Continue(_) => {
764 ctx.diverges = true;
767 }
768
769 StmtKind::Unset(vars) => {
771 for var in vars.iter() {
772 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
773 ctx.unset_var(name.as_str().trim_start_matches('$'));
774 }
775 }
776 }
777
778 StmtKind::StaticVar(vars) => {
780 for sv in vars.iter() {
781 let ty = Union::mixed(); ctx.set_var(sv.name.trim_start_matches('$'), ty);
783 }
784 }
785
786 StmtKind::Global(vars) => {
788 for var in vars.iter() {
789 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
790 let var_name = name.as_str().trim_start_matches('$');
791 let ty = self
792 .codebase
793 .global_vars
794 .get(var_name)
795 .map(|r| r.clone())
796 .unwrap_or_else(Union::mixed);
797 ctx.set_var(var_name, ty);
798 }
799 }
800 }
801
802 StmtKind::Declare(d) => {
804 for (name, _val) in d.directives.iter() {
805 if *name == "strict_types" {
806 ctx.strict_types = true;
807 }
808 }
809 if let Some(body) = &d.body {
810 self.analyze_stmt(body, ctx);
811 }
812 }
813
814 StmtKind::Function(_)
816 | StmtKind::Class(_)
817 | StmtKind::Interface(_)
818 | StmtKind::Trait(_)
819 | StmtKind::Enum(_) => {
820 }
822
823 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
825
826 StmtKind::InlineHtml(_)
828 | StmtKind::Nop
829 | StmtKind::Goto(_)
830 | StmtKind::Label(_)
831 | StmtKind::HaltCompiler(_) => {}
832
833 StmtKind::Error => {}
834 }
835 }
836
837 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
842 where
843 'a: 'b,
844 {
845 ExpressionAnalyzer::new(
846 self.codebase,
847 self.file.clone(),
848 self.source,
849 self.source_map,
850 self.issues,
851 self.symbols,
852 )
853 }
854
855 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
858 let lc = self.source_map.offset_to_line_col(offset);
859 let line = lc.line + 1;
860
861 let byte_offset = offset as usize;
862 let line_start_byte = if byte_offset == 0 {
863 0
864 } else {
865 self.source[..byte_offset]
866 .rfind('\n')
867 .map(|p| p + 1)
868 .unwrap_or(0)
869 };
870
871 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
872
873 (line, col)
874 }
875
876 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
883 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
884 return vec![];
885 };
886 let mut suppressions = Vec::new();
887 for line in doc.lines() {
888 let line = line.trim().trim_start_matches('*').trim();
889 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
890 r
891 } else if let Some(r) = line.strip_prefix("@suppress ") {
892 r
893 } else {
894 continue;
895 };
896 for name in rest.split_whitespace() {
897 suppressions.push(name.to_string());
898 }
899 }
900 suppressions
901 }
902
903 fn extract_var_annotation(
907 &self,
908 span: php_ast::Span,
909 ) -> Option<(Option<String>, mir_types::Union)> {
910 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
911 let parsed = crate::parser::DocblockParser::parse(&doc);
912 let ty = parsed.var_type?;
913 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
914 Some((parsed.var_name, resolved))
915 }
916
917 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
932 where
933 F: FnMut(&mut Self, &mut Context),
934 {
935 const MAX_ITERS: usize = 3;
936
937 self.break_ctx_stack.push(Vec::new());
939
940 let mut current = entry;
941 current.inside_loop = true;
942
943 for _ in 0..MAX_ITERS {
944 let prev_vars = current.vars.clone();
945
946 let mut iter = current.clone();
947 body(self, &mut iter);
948
949 let next = Context::merge_branches(pre, iter, None);
950
951 if vars_stabilized(&prev_vars, &next.vars) {
952 current = next;
953 break;
954 }
955 current = next;
956 }
957
958 widen_unstable(&pre.vars, &mut current.vars);
960
961 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
963 for bctx in break_ctxs {
964 current = Context::merge_branches(pre, current, Some(bctx));
965 }
966
967 current
968 }
969}
970
971fn vars_stabilized(
978 prev: &indexmap::IndexMap<String, Union>,
979 next: &indexmap::IndexMap<String, Union>,
980) -> bool {
981 if prev.len() != next.len() {
982 return false;
983 }
984 prev.iter()
985 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
986}
987
988fn widen_unstable(
991 pre_vars: &indexmap::IndexMap<String, Union>,
992 current_vars: &mut indexmap::IndexMap<String, Union>,
993) {
994 for (name, ty) in current_vars.iter_mut() {
995 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
996 *ty = Union::mixed();
997 }
998 }
999}
1000
1001fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
1006 if arr_ty.is_mixed() {
1007 return (Union::mixed(), Union::mixed());
1008 }
1009 for atomic in &arr_ty.types {
1010 match atomic {
1011 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
1012 return (*key.clone(), *value.clone());
1013 }
1014 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1015 return (Union::single(Atomic::TInt), *value.clone());
1016 }
1017 Atomic::TKeyedArray { properties, .. } => {
1018 let mut values = Union::empty();
1019 for (_k, prop) in properties {
1020 values = Union::merge(&values, &prop.ty);
1021 }
1022 let values = if values.is_empty() {
1025 Union::mixed()
1026 } else {
1027 values
1028 };
1029 return (Union::single(Atomic::TMixed), values);
1030 }
1031 Atomic::TString => {
1032 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
1033 }
1034 _ => {}
1035 }
1036 }
1037 (Union::mixed(), Union::mixed())
1038}
1039
1040fn named_object_return_compatible(
1047 actual: &Union,
1048 declared: &Union,
1049 codebase: &Codebase,
1050 file: &str,
1051) -> bool {
1052 actual.types.iter().all(|actual_atom| {
1053 let actual_fqcn: &Arc<str> = match actual_atom {
1055 Atomic::TNamedObject { fqcn, .. } => fqcn,
1056 Atomic::TSelf { fqcn } => fqcn,
1057 Atomic::TStaticObject { fqcn } => fqcn,
1058 Atomic::TParent { fqcn } => fqcn,
1059 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1061 Atomic::TVoid => {
1063 return declared
1064 .types
1065 .iter()
1066 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1067 }
1068 Atomic::TNever => return true,
1070 Atomic::TClassString(Some(actual_cls)) => {
1072 return declared.types.iter().any(|d| match d {
1073 Atomic::TClassString(None) => true,
1074 Atomic::TClassString(Some(declared_cls)) => {
1075 actual_cls == declared_cls
1076 || codebase
1077 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1078 }
1079 Atomic::TString => true,
1080 _ => false,
1081 });
1082 }
1083 Atomic::TClassString(None) => {
1084 return declared
1085 .types
1086 .iter()
1087 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1088 }
1089 _ => return false,
1091 };
1092
1093 declared.types.iter().any(|declared_atom| {
1094 let declared_fqcn: &Arc<str> = match declared_atom {
1096 Atomic::TNamedObject { fqcn, .. } => fqcn,
1097 Atomic::TSelf { fqcn } => fqcn,
1098 Atomic::TStaticObject { fqcn } => fqcn,
1099 Atomic::TParent { fqcn } => fqcn,
1100 _ => return false,
1101 };
1102
1103 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1104 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1105
1106 if matches!(
1108 actual_atom,
1109 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1110 ) && (resolved_actual == resolved_declared
1111 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1112 || actual_fqcn.as_ref() == resolved_declared.as_str()
1113 || resolved_actual.as_str() == declared_fqcn.as_ref()
1114 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1115 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1116 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1117 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1118 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1121 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1122 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1123 {
1124 return true;
1125 }
1126
1127 let is_same_class = resolved_actual == resolved_declared
1129 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1130 || actual_fqcn.as_ref() == resolved_declared.as_str()
1131 || resolved_actual.as_str() == declared_fqcn.as_ref();
1132
1133 if is_same_class {
1134 let actual_type_params = match actual_atom {
1135 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1136 _ => &[],
1137 };
1138 let declared_type_params = match declared_atom {
1139 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1140 _ => &[],
1141 };
1142 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1143 let class_tps = codebase.get_class_template_params(&resolved_declared);
1144 return return_type_params_compatible(
1145 actual_type_params,
1146 declared_type_params,
1147 &class_tps,
1148 );
1149 }
1150 return true;
1151 }
1152
1153 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1155 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1156 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1157 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1158 })
1159 })
1160}
1161
1162fn return_type_params_compatible(
1166 actual_params: &[Union],
1167 declared_params: &[Union],
1168 template_params: &[mir_codebase::storage::TemplateParam],
1169) -> bool {
1170 if actual_params.len() != declared_params.len() {
1171 return true;
1172 }
1173 if actual_params.is_empty() {
1174 return true;
1175 }
1176
1177 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1178 {
1179 let variance = template_params
1180 .get(i)
1181 .map(|tp| tp.variance)
1182 .unwrap_or(mir_types::Variance::Invariant);
1183
1184 let compatible = match variance {
1185 mir_types::Variance::Covariant => {
1186 actual_p.is_subtype_of_simple(declared_p)
1187 || declared_p.is_mixed()
1188 || actual_p.is_mixed()
1189 }
1190 mir_types::Variance::Contravariant => {
1191 declared_p.is_subtype_of_simple(actual_p)
1192 || actual_p.is_mixed()
1193 || declared_p.is_mixed()
1194 }
1195 mir_types::Variance::Invariant => {
1196 actual_p == declared_p
1197 || actual_p.is_mixed()
1198 || declared_p.is_mixed()
1199 || (actual_p.is_subtype_of_simple(declared_p)
1200 && declared_p.is_subtype_of_simple(actual_p))
1201 }
1202 };
1203
1204 if !compatible {
1205 return false;
1206 }
1207 }
1208
1209 true
1210}
1211
1212fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1216 declared.types.iter().any(|atomic| match atomic {
1217 Atomic::TTemplateParam { .. } => true,
1218 Atomic::TNamedObject { fqcn, type_params } => {
1224 !type_params.is_empty()
1225 || !codebase.type_exists(fqcn.as_ref())
1226 || codebase.interfaces.contains_key(fqcn.as_ref())
1227 }
1228 Atomic::TArray { value, .. }
1229 | Atomic::TList { value }
1230 | Atomic::TNonEmptyArray { value, .. }
1231 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1232 Atomic::TTemplateParam { .. } => true,
1233 Atomic::TNamedObject { fqcn, .. } => {
1234 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1235 }
1236 _ => false,
1237 }),
1238 _ => false,
1239 })
1240}
1241
1242fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1245 let mut result = Union::empty();
1246 result.possibly_undefined = union.possibly_undefined;
1247 result.from_docblock = union.from_docblock;
1248 for atomic in union.types {
1249 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1250 result.types.push(resolved);
1251 }
1252 result
1253}
1254
1255fn is_resolvable_class_name(s: &str) -> bool {
1256 !s.is_empty()
1257 && s.chars()
1258 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1259}
1260
1261fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1262 match atomic {
1263 Atomic::TNamedObject { fqcn, type_params } => {
1264 if !is_resolvable_class_name(fqcn.as_ref()) {
1265 return Atomic::TNamedObject { fqcn, type_params };
1266 }
1267 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1268 Atomic::TNamedObject {
1269 fqcn: resolved.into(),
1270 type_params,
1271 }
1272 }
1273 Atomic::TClassString(Some(cls)) => {
1274 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1275 Atomic::TClassString(Some(resolved.into()))
1276 }
1277 Atomic::TList { value } => Atomic::TList {
1278 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1279 },
1280 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1281 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1282 },
1283 Atomic::TArray { key, value } => Atomic::TArray {
1284 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1285 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1286 },
1287 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1288 Atomic::TSelf { fqcn }
1290 }
1291 other => other,
1292 }
1293}
1294
1295fn return_arrays_compatible(
1298 actual: &Union,
1299 declared: &Union,
1300 codebase: &Codebase,
1301 file: &str,
1302) -> bool {
1303 actual.types.iter().all(|a_atomic| {
1304 let act_val: &Union = match a_atomic {
1305 Atomic::TArray { value, .. }
1306 | Atomic::TNonEmptyArray { value, .. }
1307 | Atomic::TList { value }
1308 | Atomic::TNonEmptyList { value } => value,
1309 Atomic::TKeyedArray { .. } => return true,
1310 _ => return false,
1311 };
1312
1313 declared.types.iter().any(|d_atomic| {
1314 let dec_val: &Union = match d_atomic {
1315 Atomic::TArray { value, .. }
1316 | Atomic::TNonEmptyArray { value, .. }
1317 | Atomic::TList { value }
1318 | Atomic::TNonEmptyList { value } => value,
1319 _ => return false,
1320 };
1321
1322 act_val.types.iter().all(|av| {
1323 match av {
1324 Atomic::TNever => return true,
1325 Atomic::TClassString(Some(av_cls)) => {
1326 return dec_val.types.iter().any(|dv| match dv {
1327 Atomic::TClassString(None) | Atomic::TString => true,
1328 Atomic::TClassString(Some(dv_cls)) => {
1329 av_cls == dv_cls
1330 || codebase
1331 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1332 }
1333 _ => false,
1334 });
1335 }
1336 _ => {}
1337 }
1338 let av_fqcn: &Arc<str> = match av {
1339 Atomic::TNamedObject { fqcn, .. } => fqcn,
1340 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1341 Atomic::TClosure { .. } => return true,
1342 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1343 };
1344 dec_val.types.iter().any(|dv| {
1345 let dv_fqcn: &Arc<str> = match dv {
1346 Atomic::TNamedObject { fqcn, .. } => fqcn,
1347 Atomic::TClosure { .. } => return true,
1348 _ => return false,
1349 };
1350 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1351 return true; }
1353 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1354 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1355 res_dec == res_act
1356 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1357 || codebase.extends_or_implements(&res_act, &res_dec)
1358 })
1359 })
1360 })
1361 })
1362}