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,55 @@ 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 {
39- 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- ];
43+ // A trait's catch block can be dead in the context of one class using the trait
44+ // and alive in the context of another, e.g. when it depends on whether an
45+ // abstract method gets overridden. The key below identifies this specific catch
46+ // type occurrence so its verdicts can be compared across all classes using the trait.
47+ $ key = sprintf ( ' %s:%d ' , $ node -> getOriginalCaughtType ()-> describe ( VerbosityLevel :: typeOnly ()), $ node -> getOriginalNode ()-> getStartLine ());
48+
49+ if ( $ node -> isMatched ()) {
50+ $ this -> constantConditionInTraitHelper -> emitNoErrorForKey ( self ::class, $ scope , $ key );
51+ return [ ];
4852 }
4953
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 ;
54+ if ($ node ->getCaughtType () instanceof NeverType) {
55+ $ error = RuleErrorBuilder ::message (
56+ sprintf ('Dead catch - %s is already caught above. ' , $ node ->getOriginalCaughtType ()->describe (VerbosityLevel ::typeOnly ())),
57+ )
58+ ->line ($ node ->getStartLine ())
59+ ->identifier ('catch.alreadyCaught ' )
60+ ->build ();
61+ } else {
62+ if (!$ this ->reportUncheckedExceptionDeadCatch ) {
63+ $ isCheckedException = false ;
64+ foreach ($ node ->getCaughtType ()->getObjectClassNames () as $ objectClassName ) {
65+ if ($ this ->exceptionTypeResolver ->isCheckedException ($ objectClassName , $ scope )) {
66+ $ isCheckedException = true ;
67+ break ;
68+ }
5669 }
57- }
5870
59- if (!$ isCheckedException ) {
60- return [];
71+ if (!$ isCheckedException ) {
72+ return [];
73+ }
6174 }
62- }
6375
64- return [
65- RuleErrorBuilder ::message (
76+ $ error = RuleErrorBuilder ::message (
6677 sprintf ('Dead catch - %s is never thrown in the try block. ' , $ node ->getCaughtType ()->describe (VerbosityLevel ::typeOnly ())),
6778 )
6879 ->line ($ node ->getStartLine ())
6980 ->identifier ('catch.neverThrown ' )
70- ->build (),
71- ];
81+ ->build ();
82+ }
83+
84+ if ($ scope ->isInTrait ()) {
85+ $ this ->constantConditionInTraitHelper ->emitErrorForKey (self ::class, $ scope , $ node ->getOriginalNode (), $ key , true , $ error );
86+ return [];
87+ }
88+
89+ return [$ error ];
7290 }
7391
7492}
0 commit comments