33namespace PHPStan \Rules \Exceptions ;
44
55use PhpParser \Node ;
6+ use PHPStan \Analyser \CollectedDataEmitter ;
7+ use PHPStan \Analyser \NodeCallbackInvoker ;
68use PHPStan \Analyser \Scope ;
79use PHPStan \DependencyInjection \AutowiredParameter ;
810use PHPStan \DependencyInjection \RegisteredRule ;
911use PHPStan \Node \CatchWithUnthrownExceptionNode ;
12+ use PHPStan \Rules \Comparison \ConstantConditionInTraitHelper ;
1013use PHPStan \Rules \Rule ;
1114use PHPStan \Rules \RuleErrorBuilder ;
1215use PHPStan \Type \NeverType ;
@@ -25,6 +28,7 @@ public function __construct(
2528 private ExceptionTypeResolver $ exceptionTypeResolver ,
2629 #[AutowiredParameter(ref: '%exceptions.reportUncheckedExceptionDeadCatch% ' )]
2730 private bool $ reportUncheckedExceptionDeadCatch ,
31+ private ConstantConditionInTraitHelper $ constantConditionInTraitHelper ,
2832 )
2933 {
3034 }
@@ -34,41 +38,56 @@ public function getNodeType(): string
3438 return CatchWithUnthrownExceptionNode::class;
3539 }
3640
37- public function processNode (Node $ node , Scope $ scope ): array
41+ public function processNode (Node $ node , Scope & NodeCallbackInvoker & CollectedDataEmitter $ scope ): array
3842 {
3943 if ($ node ->getCaughtType () instanceof NeverType) {
40- return [
41- RuleErrorBuilder::message (
42- sprintf ('Dead catch - %s is already caught above. ' , $ node ->getOriginalCaughtType ()->describe (VerbosityLevel::typeOnly ())),
43- )
44- ->line ($ node ->getStartLine ())
45- ->identifier ('catch.alreadyCaught ' )
46- ->build (),
47- ];
48- }
44+ $ error = RuleErrorBuilder::message (
45+ sprintf ('Dead catch - %s is already caught above. ' , $ node ->getOriginalCaughtType ()->describe (VerbosityLevel::typeOnly ())),
46+ )
47+ ->line ($ node ->getStartLine ())
48+ ->identifier ('catch.alreadyCaught ' )
49+ ->build ();
50+ } else {
51+ if (!$ this ->reportUncheckedExceptionDeadCatch ) {
52+ $ isCheckedException = false ;
53+ foreach ($ node ->getCaughtType ()->getObjectClassNames () as $ objectClassName ) {
54+ if ($ this ->exceptionTypeResolver ->isCheckedException ($ objectClassName , $ scope )) {
55+ $ isCheckedException = true ;
56+ break ;
57+ }
58+ }
4959
50- if (!$ this ->reportUncheckedExceptionDeadCatch ) {
51- $ isCheckedException = false ;
52- foreach ($ node ->getCaughtType ()->getObjectClassNames () as $ objectClassName ) {
53- if ($ this ->exceptionTypeResolver ->isCheckedException ($ objectClassName , $ scope )) {
54- $ isCheckedException = true ;
55- break ;
60+ if (!$ isCheckedException ) {
61+ return [];
5662 }
5763 }
5864
59- if (!$ isCheckedException ) {
60- return [];
61- }
62- }
63-
64- return [
65- RuleErrorBuilder::message (
65+ $ error = RuleErrorBuilder::message (
6666 sprintf ('Dead catch - %s is never thrown in the try block. ' , $ node ->getCaughtType ()->describe (VerbosityLevel::typeOnly ())),
6767 )
6868 ->line ($ node ->getStartLine ())
6969 ->identifier ('catch.neverThrown ' )
70- ->build (),
71- ];
70+ ->build ();
71+ }
72+
73+ if ($ scope ->isInTrait ()) {
74+ // A trait's catch can be dead in the context of one class using the trait and
75+ // alive in the context of another, e.g. when it depends on whether an abstract
76+ // method gets overridden. Let the collector compare the verdicts of all the
77+ // classes using the trait instead of reporting right away; the alive ones are
78+ // recorded by CatchWithThrownExceptionInTraitRule under the same key.
79+ $ this ->constantConditionInTraitHelper ->emitErrorForKey (
80+ self ::class,
81+ $ scope ,
82+ $ node ->getOriginalNode (),
83+ DeadCatchInTraitKey::create ($ node ->getOriginalNode (), $ node ->getOriginalCaughtType ()),
84+ true ,
85+ $ error ,
86+ );
87+ return [];
88+ }
89+
90+ return [$ error ];
7291 }
7392
7493}
0 commit comments