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 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_col
74 } else {
75 col_start + 1
76 };
77 self.issues.add(
78 Issue::new(
79 IssueKind::UnreachableCode,
80 Location {
81 file: self.file.clone(),
82 line,
83 col_start,
84 col_end: col_end.max(col_start + 1),
85 },
86 )
87 .with_snippet(
88 crate::parser::span_text(self.source, stmt.span).unwrap_or_default(),
89 ),
90 );
91 if !suppressions.is_empty() {
92 self.issues.suppress_range(before, &suppressions);
93 }
94 break;
95 }
96
97 let var_annotation = self.extract_var_annotation(stmt.span);
99
100 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
103 ctx.set_var(var_name.as_str(), var_ty.clone());
104 }
105
106 self.analyze_stmt(stmt, ctx);
107
108 if let Some((Some(ref var_name), ref var_ty)) = var_annotation {
112 if let php_ast::ast::StmtKind::Expression(e) = &stmt.kind {
113 if let php_ast::ast::ExprKind::Assign(a) = &e.kind {
114 if matches!(&a.op, php_ast::ast::AssignOp::Assign) {
115 if let php_ast::ast::ExprKind::Variable(lhs_name) = &a.target.kind {
116 let lhs = lhs_name.trim_start_matches('$');
117 if lhs == var_name.as_str() {
118 ctx.set_var(var_name.as_str(), var_ty.clone());
119 }
120 }
121 }
122 }
123 }
124 }
125
126 if !suppressions.is_empty() {
127 self.issues.suppress_range(before, &suppressions);
128 }
129 }
130 }
131
132 pub fn analyze_stmt<'arena, 'src>(
133 &mut self,
134 stmt: &php_ast::ast::Stmt<'arena, 'src>,
135 ctx: &mut Context,
136 ) {
137 match &stmt.kind {
138 StmtKind::Expression(expr) => {
140 let expr_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
141 if expr_ty.is_never() {
142 ctx.diverges = true;
143 }
144 if let php_ast::ast::ExprKind::FunctionCall(call) = &expr.kind {
146 if let php_ast::ast::ExprKind::Identifier(fn_name) = &call.name.kind {
147 if fn_name.eq_ignore_ascii_case("assert") {
148 if let Some(arg) = call.args.first() {
149 narrow_from_condition(
150 &arg.value,
151 ctx,
152 true,
153 self.codebase,
154 &self.file,
155 );
156 }
157 }
158 }
159 }
160 }
161
162 StmtKind::Echo(exprs) => {
164 for expr in exprs.iter() {
165 if crate::taint::is_expr_tainted(expr, ctx) {
167 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
168 let col_end = if stmt.span.start < stmt.span.end {
169 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
170 end_col
171 } else {
172 col_start
173 };
174 let mut issue = mir_issues::Issue::new(
175 IssueKind::TaintedHtml,
176 mir_issues::Location {
177 file: self.file.clone(),
178 line,
179 col_start,
180 col_end: col_end.max(col_start + 1),
181 },
182 );
183 let start = stmt.span.start as usize;
185 let end = stmt.span.end as usize;
186 if start < self.source.len() {
187 let end = end.min(self.source.len());
188 let span_text = &self.source[start..end];
189 if let Some(first_line) = span_text.lines().next() {
190 issue = issue.with_snippet(first_line.trim().to_string());
191 }
192 }
193 self.issues.add(issue);
194 }
195 self.expr_analyzer(ctx).analyze(expr, ctx);
196 }
197 }
198
199 StmtKind::Return(opt_expr) => {
201 if let Some(expr) = opt_expr {
202 let ret_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
203
204 let check_ty =
209 if let Some((None, var_ty)) = self.extract_var_annotation(stmt.span) {
210 var_ty
211 } else {
212 ret_ty.clone()
213 };
214
215 if let Some(declared) = &ctx.fn_return_type.clone() {
217 if (declared.is_void() && !check_ty.is_void() && !check_ty.is_mixed())
221 || (!check_ty.is_subtype_of_simple(declared)
222 && !declared.is_mixed()
223 && !check_ty.is_mixed()
224 && !named_object_return_compatible(&check_ty, declared, self.codebase, &self.file)
225 && (check_ty.remove_null().is_empty() || !named_object_return_compatible(&check_ty.remove_null(), declared, self.codebase, &self.file))
229 && !declared_return_has_template(declared, self.codebase)
230 && !declared_return_has_template(&check_ty, self.codebase)
231 && !return_arrays_compatible(&check_ty, declared, self.codebase, &self.file)
232 && !declared.is_subtype_of_simple(&check_ty)
234 && !declared.remove_null().is_subtype_of_simple(&check_ty)
235 && (check_ty.remove_null().is_empty() || !check_ty.remove_null().is_subtype_of_simple(declared))
240 && !check_ty.remove_false().is_subtype_of_simple(declared)
241 && !named_object_return_compatible(declared, &check_ty, self.codebase, &self.file)
244 && !named_object_return_compatible(&declared.remove_null(), &check_ty.remove_null(), self.codebase, &self.file))
245 {
246 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
247 let col_end = if stmt.span.start < stmt.span.end {
248 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
249 end_col
250 } else {
251 col_start
252 };
253 self.issues.add(
254 mir_issues::Issue::new(
255 IssueKind::InvalidReturnType {
256 expected: format!("{}", declared),
257 actual: format!("{}", ret_ty),
258 },
259 mir_issues::Location {
260 file: self.file.clone(),
261 line,
262 col_start,
263 col_end: col_end.max(col_start + 1),
264 },
265 )
266 .with_snippet(
267 crate::parser::span_text(self.source, stmt.span)
268 .unwrap_or_default(),
269 ),
270 );
271 }
272 }
273 self.return_types.push(ret_ty);
274 } else {
275 self.return_types.push(Union::single(Atomic::TVoid));
276 if let Some(declared) = &ctx.fn_return_type.clone() {
278 if !declared.is_void() && !declared.is_mixed() {
279 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
280 let col_end = if stmt.span.start < stmt.span.end {
281 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
282 end_col
283 } else {
284 col_start
285 };
286 self.issues.add(
287 mir_issues::Issue::new(
288 IssueKind::InvalidReturnType {
289 expected: format!("{}", declared),
290 actual: "void".to_string(),
291 },
292 mir_issues::Location {
293 file: self.file.clone(),
294 line,
295 col_start,
296 col_end: col_end.max(col_start + 1),
297 },
298 )
299 .with_snippet(
300 crate::parser::span_text(self.source, stmt.span)
301 .unwrap_or_default(),
302 ),
303 );
304 }
305 }
306 }
307 ctx.diverges = true;
308 }
309
310 StmtKind::Throw(expr) => {
312 let thrown_ty = self.expr_analyzer(ctx).analyze(expr, ctx);
313 for atomic in &thrown_ty.types {
315 match atomic {
316 mir_types::Atomic::TNamedObject { fqcn, .. } => {
317 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
318 let is_throwable = resolved == "Throwable"
319 || resolved == "Exception"
320 || resolved == "Error"
321 || fqcn.as_ref() == "Throwable"
322 || fqcn.as_ref() == "Exception"
323 || fqcn.as_ref() == "Error"
324 || self.codebase.extends_or_implements(&resolved, "Throwable")
325 || self.codebase.extends_or_implements(&resolved, "Exception")
326 || self.codebase.extends_or_implements(&resolved, "Error")
327 || self.codebase.extends_or_implements(fqcn, "Throwable")
328 || self.codebase.extends_or_implements(fqcn, "Exception")
329 || self.codebase.extends_or_implements(fqcn, "Error")
330 || self.codebase.has_unknown_ancestor(&resolved)
332 || self.codebase.has_unknown_ancestor(fqcn)
333 || (!self.codebase.type_exists(&resolved) && !self.codebase.type_exists(fqcn));
335 if !is_throwable {
336 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
337 let col_end = if stmt.span.start < stmt.span.end {
338 let (_end_line, end_col) =
339 self.offset_to_line_col(stmt.span.end);
340 end_col
341 } else {
342 col_start
343 };
344 self.issues.add(mir_issues::Issue::new(
345 IssueKind::InvalidThrow {
346 ty: fqcn.to_string(),
347 },
348 mir_issues::Location {
349 file: self.file.clone(),
350 line,
351 col_start,
352 col_end: col_end.max(col_start + 1),
353 },
354 ));
355 }
356 }
357 mir_types::Atomic::TSelf { fqcn }
359 | mir_types::Atomic::TStaticObject { fqcn }
360 | mir_types::Atomic::TParent { fqcn } => {
361 let resolved = self.codebase.resolve_class_name(&self.file, fqcn);
362 let is_throwable = resolved == "Throwable"
363 || resolved == "Exception"
364 || resolved == "Error"
365 || self.codebase.extends_or_implements(&resolved, "Throwable")
366 || self.codebase.extends_or_implements(&resolved, "Exception")
367 || self.codebase.extends_or_implements(&resolved, "Error")
368 || self.codebase.extends_or_implements(fqcn, "Throwable")
369 || self.codebase.extends_or_implements(fqcn, "Exception")
370 || self.codebase.extends_or_implements(fqcn, "Error")
371 || self.codebase.has_unknown_ancestor(&resolved)
372 || self.codebase.has_unknown_ancestor(fqcn);
373 if !is_throwable {
374 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
375 let col_end = if stmt.span.start < stmt.span.end {
376 let (_end_line, end_col) =
377 self.offset_to_line_col(stmt.span.end);
378 end_col
379 } else {
380 col_start
381 };
382 self.issues.add(mir_issues::Issue::new(
383 IssueKind::InvalidThrow {
384 ty: fqcn.to_string(),
385 },
386 mir_issues::Location {
387 file: self.file.clone(),
388 line,
389 col_start,
390 col_end: col_end.max(col_start + 1),
391 },
392 ));
393 }
394 }
395 mir_types::Atomic::TMixed | mir_types::Atomic::TObject => {}
396 _ => {
397 let (line, col_start) = self.offset_to_line_col(stmt.span.start);
398 let col_end = if stmt.span.start < stmt.span.end {
399 let (_end_line, end_col) = self.offset_to_line_col(stmt.span.end);
400 end_col
401 } else {
402 col_start
403 };
404 self.issues.add(mir_issues::Issue::new(
405 IssueKind::InvalidThrow {
406 ty: format!("{}", thrown_ty),
407 },
408 mir_issues::Location {
409 file: self.file.clone(),
410 line,
411 col_start,
412 col_end: col_end.max(col_start + 1),
413 },
414 ));
415 }
416 }
417 }
418 ctx.diverges = true;
419 }
420
421 StmtKind::If(if_stmt) => {
423 let pre_ctx = ctx.clone();
424
425 let cond_type = self.expr_analyzer(ctx).analyze(&if_stmt.condition, ctx);
427 let pre_diverges = ctx.diverges;
428
429 let mut then_ctx = ctx.fork();
431 narrow_from_condition(
432 &if_stmt.condition,
433 &mut then_ctx,
434 true,
435 self.codebase,
436 &self.file,
437 );
438 let then_unreachable_from_narrowing = then_ctx.diverges;
442 if !then_ctx.diverges {
445 self.analyze_stmt(if_stmt.then_branch, &mut then_ctx);
446 }
447
448 let mut elseif_ctxs: Vec<Context> = vec![];
450 for elseif in if_stmt.elseif_branches.iter() {
451 let mut pre_elseif = ctx.fork();
454 narrow_from_condition(
455 &if_stmt.condition,
456 &mut pre_elseif,
457 false,
458 self.codebase,
459 &self.file,
460 );
461 let pre_elseif_diverges = pre_elseif.diverges;
462
463 let mut elseif_true_ctx = pre_elseif.clone();
467 narrow_from_condition(
468 &elseif.condition,
469 &mut elseif_true_ctx,
470 true,
471 self.codebase,
472 &self.file,
473 );
474 let mut elseif_false_ctx = pre_elseif.clone();
475 narrow_from_condition(
476 &elseif.condition,
477 &mut elseif_false_ctx,
478 false,
479 self.codebase,
480 &self.file,
481 );
482 if !pre_elseif_diverges
483 && (elseif_true_ctx.diverges || elseif_false_ctx.diverges)
484 {
485 let (line, col_start) =
486 self.offset_to_line_col(elseif.condition.span.start);
487 let col_end = if elseif.condition.span.start < elseif.condition.span.end {
488 let (_end_line, end_col) =
489 self.offset_to_line_col(elseif.condition.span.end);
490 end_col
491 } else {
492 col_start
493 };
494 let elseif_cond_type = self
495 .expr_analyzer(ctx)
496 .analyze(&elseif.condition, &mut ctx.fork());
497 self.issues.add(
498 mir_issues::Issue::new(
499 IssueKind::RedundantCondition {
500 ty: format!("{}", elseif_cond_type),
501 },
502 mir_issues::Location {
503 file: self.file.clone(),
504 line,
505 col_start,
506 col_end: col_end.max(col_start + 1),
507 },
508 )
509 .with_snippet(
510 crate::parser::span_text(self.source, elseif.condition.span)
511 .unwrap_or_default(),
512 ),
513 );
514 }
515
516 let mut branch_ctx = elseif_true_ctx;
518 self.expr_analyzer(&branch_ctx)
519 .analyze(&elseif.condition, &mut branch_ctx);
520 if !branch_ctx.diverges {
521 self.analyze_stmt(&elseif.body, &mut branch_ctx);
522 }
523 elseif_ctxs.push(branch_ctx);
524 }
525
526 let mut else_ctx = ctx.fork();
528 narrow_from_condition(
529 &if_stmt.condition,
530 &mut else_ctx,
531 false,
532 self.codebase,
533 &self.file,
534 );
535 let else_unreachable_from_narrowing = else_ctx.diverges;
536 if !else_ctx.diverges {
537 if let Some(else_branch) = &if_stmt.else_branch {
538 self.analyze_stmt(else_branch, &mut else_ctx);
539 }
540 }
541
542 if !pre_diverges
544 && (then_unreachable_from_narrowing || else_unreachable_from_narrowing)
545 {
546 let (line, col_start) = self.offset_to_line_col(if_stmt.condition.span.start);
547 let col_end = if if_stmt.condition.span.start < if_stmt.condition.span.end {
548 let (_end_line, end_col) =
549 self.offset_to_line_col(if_stmt.condition.span.end);
550 end_col
551 } else {
552 col_start
553 };
554 self.issues.add(
555 mir_issues::Issue::new(
556 IssueKind::RedundantCondition {
557 ty: format!("{}", cond_type),
558 },
559 mir_issues::Location {
560 file: self.file.clone(),
561 line,
562 col_start,
563 col_end: col_end.max(col_start + 1),
564 },
565 )
566 .with_snippet(
567 crate::parser::span_text(self.source, if_stmt.condition.span)
568 .unwrap_or_default(),
569 ),
570 );
571 }
572
573 *ctx = Context::merge_branches(&pre_ctx, then_ctx, Some(else_ctx));
578 for ec in elseif_ctxs {
579 *ctx = Context::merge_branches(&pre_ctx, ec, Some(ctx.clone()));
580 }
581 }
582
583 StmtKind::While(w) => {
585 self.expr_analyzer(ctx).analyze(&w.condition, ctx);
586 let pre = ctx.clone();
587
588 let mut entry = ctx.fork();
590 narrow_from_condition(&w.condition, &mut entry, true, self.codebase, &self.file);
591
592 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
593 sa.analyze_stmt(w.body, iter);
594 sa.expr_analyzer(iter).analyze(&w.condition, iter);
595 });
596 *ctx = post;
597 }
598
599 StmtKind::DoWhile(dw) => {
601 let pre = ctx.clone();
602 let entry = ctx.fork();
603 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
604 sa.analyze_stmt(dw.body, iter);
605 sa.expr_analyzer(iter).analyze(&dw.condition, iter);
606 });
607 *ctx = post;
608 }
609
610 StmtKind::For(f) => {
612 for init in f.init.iter() {
614 self.expr_analyzer(ctx).analyze(init, ctx);
615 }
616 let pre = ctx.clone();
617 let mut entry = ctx.fork();
618 for cond in f.condition.iter() {
619 self.expr_analyzer(&entry).analyze(cond, &mut entry);
620 }
621
622 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
623 sa.analyze_stmt(f.body, iter);
624 for update in f.update.iter() {
625 sa.expr_analyzer(iter).analyze(update, iter);
626 }
627 for cond in f.condition.iter() {
628 sa.expr_analyzer(iter).analyze(cond, iter);
629 }
630 });
631 *ctx = post;
632 }
633
634 StmtKind::Foreach(fe) => {
636 let arr_ty = self.expr_analyzer(ctx).analyze(&fe.expr, ctx);
637 let (key_ty, mut value_ty) = infer_foreach_types(&arr_ty);
638
639 if let Some(vname) = crate::expr::extract_simple_var(&fe.value) {
642 if let Some((Some(ann_var), ann_ty)) = self.extract_var_annotation(stmt.span) {
643 if ann_var == vname {
644 value_ty = ann_ty;
645 }
646 }
647 }
648
649 let pre = ctx.clone();
650 let mut entry = ctx.fork();
651
652 if let Some(key_expr) = &fe.key {
654 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
655 entry.set_var(var_name, key_ty.clone());
656 }
657 }
658 let value_var = crate::expr::extract_simple_var(&fe.value);
661 let value_destructure_vars = crate::expr::extract_destructure_vars(&fe.value);
662 if let Some(ref vname) = value_var {
663 entry.set_var(vname.as_str(), value_ty.clone());
664 } else {
665 for vname in &value_destructure_vars {
666 entry.set_var(vname, Union::mixed());
667 }
668 }
669
670 let post = self.analyze_loop_widened(&pre, entry, |sa, iter| {
671 if let Some(key_expr) = &fe.key {
673 if let Some(var_name) = crate::expr::extract_simple_var(key_expr) {
674 iter.set_var(var_name, key_ty.clone());
675 }
676 }
677 if let Some(ref vname) = value_var {
678 iter.set_var(vname.as_str(), value_ty.clone());
679 } else {
680 for vname in &value_destructure_vars {
681 iter.set_var(vname, Union::mixed());
682 }
683 }
684 sa.analyze_stmt(fe.body, iter);
685 });
686 *ctx = post;
687 }
688
689 StmtKind::Switch(sw) => {
691 let _subject_ty = self.expr_analyzer(ctx).analyze(&sw.expr, ctx);
692 let subject_var: Option<String> = match &sw.expr.kind {
694 php_ast::ast::ExprKind::Variable(name) => {
695 Some(name.as_str().trim_start_matches('$').to_string())
696 }
697 _ => None,
698 };
699 let switch_on_true = matches!(&sw.expr.kind, php_ast::ast::ExprKind::Bool(true));
701
702 let pre_ctx = ctx.clone();
703 self.break_ctx_stack.push(Vec::new());
706
707 let has_default = sw.cases.iter().any(|c| c.value.is_none());
708
709 let mut case_results: Vec<Context> = Vec::new();
713 for case in sw.cases.iter() {
714 let mut case_ctx = pre_ctx.fork();
715 if let Some(val) = &case.value {
716 if switch_on_true {
717 narrow_from_condition(
719 val,
720 &mut case_ctx,
721 true,
722 self.codebase,
723 &self.file,
724 );
725 } else if let Some(ref var_name) = subject_var {
726 let narrow_ty = match &val.kind {
728 php_ast::ast::ExprKind::Int(n) => {
729 Some(Union::single(Atomic::TLiteralInt(*n)))
730 }
731 php_ast::ast::ExprKind::String(s) => {
732 Some(Union::single(Atomic::TLiteralString(Arc::from(&**s))))
733 }
734 php_ast::ast::ExprKind::Bool(b) => Some(Union::single(if *b {
735 Atomic::TTrue
736 } else {
737 Atomic::TFalse
738 })),
739 php_ast::ast::ExprKind::Null => Some(Union::single(Atomic::TNull)),
740 _ => None,
741 };
742 if let Some(narrowed) = narrow_ty {
743 case_ctx.set_var(var_name, narrowed);
744 }
745 }
746 self.expr_analyzer(&case_ctx).analyze(val, &mut case_ctx);
747 }
748 self.analyze_stmts(&case.body, &mut case_ctx);
749 case_results.push(case_ctx);
750 }
751
752 let n = case_results.len();
764 let mut effective_diverges = vec![false; n];
765 for i in (0..n).rev() {
766 if case_results[i].diverges {
767 effective_diverges[i] = true;
768 } else if i + 1 < n {
769 effective_diverges[i] = effective_diverges[i + 1];
771 }
772 }
774
775 let mut all_cases_diverge = true;
778 let mut fallthrough_ctxs: Vec<Context> = Vec::new();
779 for (i, case_ctx) in case_results.into_iter().enumerate() {
780 if !effective_diverges[i] {
781 all_cases_diverge = false;
782 fallthrough_ctxs.push(case_ctx);
783 }
784 }
785
786 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
789
790 let mut merged = if has_default
794 && all_cases_diverge
795 && break_ctxs.is_empty()
796 && fallthrough_ctxs.is_empty()
797 {
798 let mut m = pre_ctx.clone();
800 m.diverges = true;
801 m
802 } else {
803 pre_ctx.clone()
806 };
807
808 for bctx in break_ctxs {
809 merged = Context::merge_branches(&pre_ctx, bctx, Some(merged));
810 }
811 for fctx in fallthrough_ctxs {
812 merged = Context::merge_branches(&pre_ctx, fctx, Some(merged));
813 }
814
815 *ctx = merged;
816 }
817
818 StmtKind::TryCatch(tc) => {
820 let pre_ctx = ctx.clone();
821 let mut try_ctx = ctx.fork();
822 self.analyze_stmts(&tc.body, &mut try_ctx);
823
824 let catch_base = Context::merge_branches(&pre_ctx, try_ctx.clone(), None);
828
829 let mut non_diverging_catches: Vec<Context> = vec![];
830 for catch in tc.catches.iter() {
831 let mut catch_ctx = catch_base.clone();
832 for catch_ty in catch.types.iter() {
834 self.check_name_undefined_class(catch_ty);
835 }
836 if let Some(var) = catch.var {
837 let exc_ty = if catch.types.is_empty() {
839 Union::single(Atomic::TObject)
840 } else {
841 let mut u = Union::empty();
842 for catch_ty in catch.types.iter() {
843 let raw = crate::parser::name_to_string(catch_ty);
844 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
845 u.add_type(Atomic::TNamedObject {
846 fqcn: resolved.into(),
847 type_params: vec![],
848 });
849 }
850 u
851 };
852 catch_ctx.set_var(var.trim_start_matches('$'), exc_ty);
853 }
854 self.analyze_stmts(&catch.body, &mut catch_ctx);
855 if !catch_ctx.diverges {
856 non_diverging_catches.push(catch_ctx);
857 }
858 }
859
860 let mut result = if non_diverging_catches.is_empty() {
864 let mut r = try_ctx;
865 r.diverges = false; r
867 } else {
868 let mut r = try_ctx;
871 for catch_ctx in non_diverging_catches {
872 r = Context::merge_branches(&pre_ctx, r, Some(catch_ctx));
873 }
874 r
875 };
876
877 if let Some(finally_stmts) = &tc.finally {
879 let mut finally_ctx = result.clone();
880 finally_ctx.inside_finally = true;
881 self.analyze_stmts(finally_stmts, &mut finally_ctx);
882 if finally_ctx.diverges {
883 result.diverges = true;
884 }
885 }
886
887 *ctx = result;
888 }
889
890 StmtKind::Block(stmts) => {
892 self.analyze_stmts(stmts, ctx);
893 }
894
895 StmtKind::Break(_) => {
897 if let Some(break_ctxs) = self.break_ctx_stack.last_mut() {
900 break_ctxs.push(ctx.clone());
901 }
902 ctx.diverges = true;
905 }
906
907 StmtKind::Continue(_) => {
909 ctx.diverges = true;
912 }
913
914 StmtKind::Unset(vars) => {
916 for var in vars.iter() {
917 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
918 ctx.unset_var(name.as_str().trim_start_matches('$'));
919 }
920 }
921 }
922
923 StmtKind::StaticVar(vars) => {
925 for sv in vars.iter() {
926 let ty = Union::mixed(); ctx.set_var(sv.name.trim_start_matches('$'), ty);
928 }
929 }
930
931 StmtKind::Global(vars) => {
933 for var in vars.iter() {
934 if let php_ast::ast::ExprKind::Variable(name) = &var.kind {
935 let var_name = name.as_str().trim_start_matches('$');
936 let ty = self
937 .codebase
938 .global_vars
939 .get(var_name)
940 .map(|r| r.clone())
941 .unwrap_or_else(Union::mixed);
942 ctx.set_var(var_name, ty);
943 }
944 }
945 }
946
947 StmtKind::Declare(d) => {
949 for (name, _val) in d.directives.iter() {
950 if *name == "strict_types" {
951 ctx.strict_types = true;
952 }
953 }
954 if let Some(body) = &d.body {
955 self.analyze_stmt(body, ctx);
956 }
957 }
958
959 StmtKind::Function(decl) => {
961 let params: Vec<mir_codebase::FnParam> = decl
964 .params
965 .iter()
966 .map(|p| mir_codebase::FnParam {
967 name: std::sync::Arc::from(p.name.trim_start_matches('$')),
968 ty: None,
969 default: p.default.as_ref().map(|_| Union::mixed()),
970 is_variadic: p.variadic,
971 is_byref: p.by_ref,
972 is_optional: p.default.is_some() || p.variadic,
973 })
974 .collect();
975 let mut fn_ctx =
976 Context::for_function(¶ms, None, None, None, None, ctx.strict_types, true);
977 let mut sa = StatementsAnalyzer::new(
978 self.codebase,
979 self.file.clone(),
980 self.source,
981 self.source_map,
982 self.issues,
983 self.symbols,
984 self.php_version,
985 );
986 sa.analyze_stmts(&decl.body, &mut fn_ctx);
987 }
988
989 StmtKind::Class(decl) => {
990 let class_name = decl.name.unwrap_or("<anonymous>");
993 let resolved = self.codebase.resolve_class_name(&self.file, class_name);
994 let fqcn: Arc<str> = Arc::from(resolved.as_str());
995 let parent_fqcn = self
996 .codebase
997 .classes
998 .get(fqcn.as_ref())
999 .and_then(|c| c.parent.clone());
1000
1001 for member in decl.members.iter() {
1002 let php_ast::ast::ClassMemberKind::Method(method) = &member.kind else {
1003 continue;
1004 };
1005 let Some(body) = &method.body else { continue };
1006 let (params, return_ty) = self
1007 .codebase
1008 .get_method(fqcn.as_ref(), method.name)
1009 .as_deref()
1010 .map(|m| (m.params.clone(), m.return_type.clone()))
1011 .unwrap_or_else(|| {
1012 let ast_params = method
1013 .params
1014 .iter()
1015 .map(|p| mir_codebase::FnParam {
1016 name: p.name.trim_start_matches('$').into(),
1017 ty: None,
1018 default: p.default.as_ref().map(|_| mir_types::Union::mixed()),
1019 is_variadic: p.variadic,
1020 is_byref: p.by_ref,
1021 is_optional: p.default.is_some() || p.variadic,
1022 })
1023 .collect();
1024 (ast_params, None)
1025 });
1026 let is_ctor = method.name == "__construct";
1027 let mut method_ctx = Context::for_method(
1028 ¶ms,
1029 return_ty,
1030 Some(fqcn.clone()),
1031 parent_fqcn.clone(),
1032 Some(fqcn.clone()),
1033 ctx.strict_types,
1034 is_ctor,
1035 method.is_static,
1036 );
1037 let mut sa = StatementsAnalyzer::new(
1038 self.codebase,
1039 self.file.clone(),
1040 self.source,
1041 self.source_map,
1042 self.issues,
1043 self.symbols,
1044 self.php_version,
1045 );
1046 sa.analyze_stmts(body, &mut method_ctx);
1047 }
1048 }
1049
1050 StmtKind::Interface(_) | StmtKind::Trait(_) | StmtKind::Enum(_) => {
1051 }
1053
1054 StmtKind::Namespace(_) | StmtKind::Use(_) | StmtKind::Const(_) => {}
1056
1057 StmtKind::InlineHtml(_)
1059 | StmtKind::Nop
1060 | StmtKind::Goto(_)
1061 | StmtKind::Label(_)
1062 | StmtKind::HaltCompiler(_) => {}
1063
1064 StmtKind::Error => {}
1065 }
1066 }
1067
1068 fn expr_analyzer<'b>(&'b mut self, _ctx: &Context) -> ExpressionAnalyzer<'b>
1073 where
1074 'a: 'b,
1075 {
1076 ExpressionAnalyzer::new(
1077 self.codebase,
1078 self.file.clone(),
1079 self.source,
1080 self.source_map,
1081 self.issues,
1082 self.symbols,
1083 self.php_version,
1084 )
1085 }
1086
1087 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1090 let lc = self.source_map.offset_to_line_col(offset);
1091 let line = lc.line + 1;
1092
1093 let byte_offset = offset as usize;
1094 let line_start_byte = if byte_offset == 0 {
1095 0
1096 } else {
1097 self.source[..byte_offset]
1098 .rfind('\n')
1099 .map(|p| p + 1)
1100 .unwrap_or(0)
1101 };
1102
1103 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1104
1105 (line, col)
1106 }
1107
1108 fn check_name_undefined_class(&mut self, name: &php_ast::ast::Name<'_, '_>) {
1110 let raw = crate::parser::name_to_string(name);
1111 let resolved = self.codebase.resolve_class_name(&self.file, &raw);
1112 if matches!(resolved.as_str(), "self" | "static" | "parent") {
1113 return;
1114 }
1115 if self.codebase.type_exists(&resolved) {
1116 return;
1117 }
1118 let span = name.span();
1119 let (line, col_start) = self.offset_to_line_col(span.start);
1120 let (_, col_end) = self.offset_to_line_col(span.end);
1121 self.issues.add(Issue::new(
1122 IssueKind::UndefinedClass { name: resolved },
1123 Location {
1124 file: self.file.clone(),
1125 line,
1126 col_start,
1127 col_end: col_end.max(col_start + 1),
1128 },
1129 ));
1130 }
1131
1132 fn extract_statement_suppressions(&self, span: php_ast::Span) -> Vec<String> {
1139 let Some(doc) = crate::parser::find_preceding_docblock(self.source, span.start) else {
1140 return vec![];
1141 };
1142 let mut suppressions = Vec::new();
1143 for line in doc.lines() {
1144 let line = line.trim().trim_start_matches('*').trim();
1145 let rest = if let Some(r) = line.strip_prefix("@psalm-suppress ") {
1146 r
1147 } else if let Some(r) = line.strip_prefix("@suppress ") {
1148 r
1149 } else {
1150 continue;
1151 };
1152 for name in rest.split_whitespace() {
1153 suppressions.push(name.to_string());
1154 }
1155 }
1156 suppressions
1157 }
1158
1159 fn extract_var_annotation(
1163 &self,
1164 span: php_ast::Span,
1165 ) -> Option<(Option<String>, mir_types::Union)> {
1166 let doc = crate::parser::find_preceding_docblock(self.source, span.start)?;
1167 let parsed = crate::parser::DocblockParser::parse(&doc);
1168 let ty = parsed.var_type?;
1169 let resolved = resolve_union_for_file(ty, self.codebase, &self.file);
1170 Some((parsed.var_name, resolved))
1171 }
1172
1173 fn analyze_loop_widened<F>(&mut self, pre: &Context, entry: Context, mut body: F) -> Context
1188 where
1189 F: FnMut(&mut Self, &mut Context),
1190 {
1191 const MAX_ITERS: usize = 3;
1192
1193 self.break_ctx_stack.push(Vec::new());
1195
1196 let mut current = entry;
1197 current.inside_loop = true;
1198
1199 for _ in 0..MAX_ITERS {
1200 let prev_vars = current.vars.clone();
1201
1202 let mut iter = current.clone();
1203 body(self, &mut iter);
1204
1205 let next = Context::merge_branches(pre, iter, None);
1206
1207 if vars_stabilized(&prev_vars, &next.vars) {
1208 current = next;
1209 break;
1210 }
1211 current = next;
1212 }
1213
1214 widen_unstable(&pre.vars, &mut current.vars);
1216
1217 let break_ctxs = self.break_ctx_stack.pop().unwrap_or_default();
1219 for bctx in break_ctxs {
1220 current = Context::merge_branches(pre, current, Some(bctx));
1221 }
1222
1223 current
1224 }
1225}
1226
1227fn vars_stabilized(
1234 prev: &indexmap::IndexMap<String, Union>,
1235 next: &indexmap::IndexMap<String, Union>,
1236) -> bool {
1237 if prev.len() != next.len() {
1238 return false;
1239 }
1240 prev.iter()
1241 .all(|(k, v)| next.get(k).map(|u| u == v).unwrap_or(false))
1242}
1243
1244fn widen_unstable(
1247 pre_vars: &indexmap::IndexMap<String, Union>,
1248 current_vars: &mut indexmap::IndexMap<String, Union>,
1249) {
1250 for (name, ty) in current_vars.iter_mut() {
1251 if pre_vars.get(name).map(|p| p != ty).unwrap_or(true) && !ty.is_mixed() {
1252 *ty = Union::mixed();
1253 }
1254 }
1255}
1256
1257fn infer_foreach_types(arr_ty: &Union) -> (Union, Union) {
1262 if arr_ty.is_mixed() {
1263 return (Union::mixed(), Union::mixed());
1264 }
1265 for atomic in &arr_ty.types {
1266 match atomic {
1267 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
1268 return (*key.clone(), *value.clone());
1269 }
1270 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1271 return (Union::single(Atomic::TInt), *value.clone());
1272 }
1273 Atomic::TKeyedArray { properties, .. } => {
1274 let mut keys = Union::empty();
1275 let mut values = Union::empty();
1276 for (k, prop) in properties {
1277 let key_atomic = match k {
1278 ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1279 ArrayKey::Int(i) => Atomic::TLiteralInt(*i),
1280 };
1281 keys = Union::merge(&keys, &Union::single(key_atomic));
1282 values = Union::merge(&values, &prop.ty);
1283 }
1284 let keys = if keys.is_empty() {
1287 Union::mixed()
1288 } else {
1289 keys
1290 };
1291 let values = if values.is_empty() {
1292 Union::mixed()
1293 } else {
1294 values
1295 };
1296 return (keys, values);
1297 }
1298 Atomic::TString => {
1299 return (Union::single(Atomic::TInt), Union::single(Atomic::TString));
1300 }
1301 _ => {}
1302 }
1303 }
1304 (Union::mixed(), Union::mixed())
1305}
1306
1307fn named_object_return_compatible(
1314 actual: &Union,
1315 declared: &Union,
1316 codebase: &Codebase,
1317 file: &str,
1318) -> bool {
1319 actual.types.iter().all(|actual_atom| {
1320 let actual_fqcn: &Arc<str> = match actual_atom {
1322 Atomic::TNamedObject { fqcn, .. } => fqcn,
1323 Atomic::TSelf { fqcn } => fqcn,
1324 Atomic::TStaticObject { fqcn } => fqcn,
1325 Atomic::TParent { fqcn } => fqcn,
1326 Atomic::TNull => return declared.types.iter().any(|d| matches!(d, Atomic::TNull)),
1328 Atomic::TVoid => {
1330 return declared
1331 .types
1332 .iter()
1333 .any(|d| matches!(d, Atomic::TVoid | Atomic::TNull))
1334 }
1335 Atomic::TNever => return true,
1337 Atomic::TClassString(Some(actual_cls)) => {
1339 return declared.types.iter().any(|d| match d {
1340 Atomic::TClassString(None) => true,
1341 Atomic::TClassString(Some(declared_cls)) => {
1342 actual_cls == declared_cls
1343 || codebase
1344 .extends_or_implements(actual_cls.as_ref(), declared_cls.as_ref())
1345 }
1346 Atomic::TString => true,
1347 _ => false,
1348 });
1349 }
1350 Atomic::TClassString(None) => {
1351 return declared
1352 .types
1353 .iter()
1354 .any(|d| matches!(d, Atomic::TClassString(_) | Atomic::TString));
1355 }
1356 _ => return false,
1358 };
1359
1360 declared.types.iter().any(|declared_atom| {
1361 let declared_fqcn: &Arc<str> = match declared_atom {
1363 Atomic::TNamedObject { fqcn, .. } => fqcn,
1364 Atomic::TSelf { fqcn } => fqcn,
1365 Atomic::TStaticObject { fqcn } => fqcn,
1366 Atomic::TParent { fqcn } => fqcn,
1367 _ => return false,
1368 };
1369
1370 let resolved_declared = codebase.resolve_class_name(file, declared_fqcn.as_ref());
1371 let resolved_actual = codebase.resolve_class_name(file, actual_fqcn.as_ref());
1372
1373 if matches!(
1375 actual_atom,
1376 Atomic::TSelf { .. } | Atomic::TStaticObject { .. }
1377 ) && (resolved_actual == resolved_declared
1378 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1379 || actual_fqcn.as_ref() == resolved_declared.as_str()
1380 || resolved_actual.as_str() == declared_fqcn.as_ref()
1381 || codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1382 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1383 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1384 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1385 || codebase.extends_or_implements(&resolved_declared, actual_fqcn.as_ref())
1388 || codebase.extends_or_implements(&resolved_declared, &resolved_actual)
1389 || codebase.extends_or_implements(declared_fqcn.as_ref(), actual_fqcn.as_ref()))
1390 {
1391 return true;
1392 }
1393
1394 let is_same_class = resolved_actual == resolved_declared
1396 || actual_fqcn.as_ref() == declared_fqcn.as_ref()
1397 || actual_fqcn.as_ref() == resolved_declared.as_str()
1398 || resolved_actual.as_str() == declared_fqcn.as_ref();
1399
1400 if is_same_class {
1401 let actual_type_params = match actual_atom {
1402 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1403 _ => &[],
1404 };
1405 let declared_type_params = match declared_atom {
1406 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1407 _ => &[],
1408 };
1409 if !actual_type_params.is_empty() || !declared_type_params.is_empty() {
1410 let class_tps = codebase.get_class_template_params(&resolved_declared);
1411 return return_type_params_compatible(
1412 actual_type_params,
1413 declared_type_params,
1414 &class_tps,
1415 );
1416 }
1417 return true;
1418 }
1419
1420 codebase.extends_or_implements(actual_fqcn.as_ref(), &resolved_declared)
1422 || codebase.extends_or_implements(actual_fqcn.as_ref(), declared_fqcn.as_ref())
1423 || codebase.extends_or_implements(&resolved_actual, &resolved_declared)
1424 || codebase.extends_or_implements(&resolved_actual, declared_fqcn.as_ref())
1425 })
1426 })
1427}
1428
1429fn return_type_params_compatible(
1433 actual_params: &[Union],
1434 declared_params: &[Union],
1435 template_params: &[mir_codebase::storage::TemplateParam],
1436) -> bool {
1437 if actual_params.len() != declared_params.len() {
1438 return true;
1439 }
1440 if actual_params.is_empty() {
1441 return true;
1442 }
1443
1444 for (i, (actual_p, declared_p)) in actual_params.iter().zip(declared_params.iter()).enumerate()
1445 {
1446 let variance = template_params
1447 .get(i)
1448 .map(|tp| tp.variance)
1449 .unwrap_or(mir_types::Variance::Invariant);
1450
1451 let compatible = match variance {
1452 mir_types::Variance::Covariant => {
1453 actual_p.is_subtype_of_simple(declared_p)
1454 || declared_p.is_mixed()
1455 || actual_p.is_mixed()
1456 }
1457 mir_types::Variance::Contravariant => {
1458 declared_p.is_subtype_of_simple(actual_p)
1459 || actual_p.is_mixed()
1460 || declared_p.is_mixed()
1461 }
1462 mir_types::Variance::Invariant => {
1463 actual_p == declared_p
1464 || actual_p.is_mixed()
1465 || declared_p.is_mixed()
1466 || (actual_p.is_subtype_of_simple(declared_p)
1467 && declared_p.is_subtype_of_simple(actual_p))
1468 }
1469 };
1470
1471 if !compatible {
1472 return false;
1473 }
1474 }
1475
1476 true
1477}
1478
1479fn declared_return_has_template(declared: &Union, codebase: &Codebase) -> bool {
1483 declared.types.iter().any(|atomic| match atomic {
1484 Atomic::TTemplateParam { .. } => true,
1485 Atomic::TNamedObject { fqcn, type_params } => {
1491 !type_params.is_empty()
1492 || !codebase.type_exists(fqcn.as_ref())
1493 || codebase.interfaces.contains_key(fqcn.as_ref())
1494 }
1495 Atomic::TArray { value, .. }
1496 | Atomic::TList { value }
1497 | Atomic::TNonEmptyArray { value, .. }
1498 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1499 Atomic::TTemplateParam { .. } => true,
1500 Atomic::TNamedObject { fqcn, .. } => {
1501 !fqcn.contains('\\') && !codebase.type_exists(fqcn.as_ref())
1502 }
1503 _ => false,
1504 }),
1505 _ => false,
1506 })
1507}
1508
1509fn resolve_union_for_file(union: Union, codebase: &Codebase, file: &str) -> Union {
1512 let mut result = Union::empty();
1513 result.possibly_undefined = union.possibly_undefined;
1514 result.from_docblock = union.from_docblock;
1515 for atomic in union.types {
1516 let resolved = resolve_atomic_for_file(atomic, codebase, file);
1517 result.types.push(resolved);
1518 }
1519 result
1520}
1521
1522fn is_resolvable_class_name(s: &str) -> bool {
1523 !s.is_empty()
1524 && s.chars()
1525 .all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
1526}
1527
1528fn resolve_atomic_for_file(atomic: Atomic, codebase: &Codebase, file: &str) -> Atomic {
1529 match atomic {
1530 Atomic::TNamedObject { fqcn, type_params } => {
1531 if !is_resolvable_class_name(fqcn.as_ref()) {
1532 return Atomic::TNamedObject { fqcn, type_params };
1533 }
1534 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1535 Atomic::TNamedObject {
1536 fqcn: resolved.into(),
1537 type_params,
1538 }
1539 }
1540 Atomic::TClassString(Some(cls)) => {
1541 let resolved = codebase.resolve_class_name(file, cls.as_ref());
1542 Atomic::TClassString(Some(resolved.into()))
1543 }
1544 Atomic::TList { value } => Atomic::TList {
1545 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1546 },
1547 Atomic::TNonEmptyList { value } => Atomic::TNonEmptyList {
1548 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1549 },
1550 Atomic::TArray { key, value } => Atomic::TArray {
1551 key: Box::new(resolve_union_for_file(*key, codebase, file)),
1552 value: Box::new(resolve_union_for_file(*value, codebase, file)),
1553 },
1554 Atomic::TSelf { fqcn } if fqcn.is_empty() => {
1555 Atomic::TSelf { fqcn }
1557 }
1558 other => other,
1559 }
1560}
1561
1562fn return_arrays_compatible(
1565 actual: &Union,
1566 declared: &Union,
1567 codebase: &Codebase,
1568 file: &str,
1569) -> bool {
1570 actual.types.iter().all(|a_atomic| {
1571 let act_val: &Union = match a_atomic {
1572 Atomic::TArray { value, .. }
1573 | Atomic::TNonEmptyArray { value, .. }
1574 | Atomic::TList { value }
1575 | Atomic::TNonEmptyList { value } => value,
1576 Atomic::TKeyedArray { .. } => return true,
1577 _ => return false,
1578 };
1579
1580 declared.types.iter().any(|d_atomic| {
1581 let dec_val: &Union = match d_atomic {
1582 Atomic::TArray { value, .. }
1583 | Atomic::TNonEmptyArray { value, .. }
1584 | Atomic::TList { value }
1585 | Atomic::TNonEmptyList { value } => value,
1586 _ => return false,
1587 };
1588
1589 act_val.types.iter().all(|av| {
1590 match av {
1591 Atomic::TNever => return true,
1592 Atomic::TClassString(Some(av_cls)) => {
1593 return dec_val.types.iter().any(|dv| match dv {
1594 Atomic::TClassString(None) | Atomic::TString => true,
1595 Atomic::TClassString(Some(dv_cls)) => {
1596 av_cls == dv_cls
1597 || codebase
1598 .extends_or_implements(av_cls.as_ref(), dv_cls.as_ref())
1599 }
1600 _ => false,
1601 });
1602 }
1603 _ => {}
1604 }
1605 let av_fqcn: &Arc<str> = match av {
1606 Atomic::TNamedObject { fqcn, .. } => fqcn,
1607 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => fqcn,
1608 Atomic::TClosure { .. } => return true,
1609 _ => return Union::single(av.clone()).is_subtype_of_simple(dec_val),
1610 };
1611 dec_val.types.iter().any(|dv| {
1612 let dv_fqcn: &Arc<str> = match dv {
1613 Atomic::TNamedObject { fqcn, .. } => fqcn,
1614 Atomic::TClosure { .. } => return true,
1615 _ => return false,
1616 };
1617 if !dv_fqcn.contains('\\') && !codebase.type_exists(dv_fqcn.as_ref()) {
1618 return true; }
1620 let res_dec = codebase.resolve_class_name(file, dv_fqcn.as_ref());
1621 let res_act = codebase.resolve_class_name(file, av_fqcn.as_ref());
1622 res_dec == res_act
1623 || codebase.extends_or_implements(av_fqcn.as_ref(), &res_dec)
1624 || codebase.extends_or_implements(&res_act, &res_dec)
1625 })
1626 })
1627 })
1628 })
1629}