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