12
12
use ShipMonk \PHPStan \DeadCode \Collector \EntrypointCollector ;
13
13
use ShipMonk \PHPStan \DeadCode \Collector \MethodCallCollector ;
14
14
use ShipMonk \PHPStan \DeadCode \Crate \Call ;
15
- use ShipMonk \PHPStan \DeadCode \Crate \Method ;
15
+ use ShipMonk \PHPStan \DeadCode \Crate \Kind ;
16
+ use ShipMonk \PHPStan \DeadCode \Crate \Visibility ;
16
17
use ShipMonk \PHPStan \DeadCode \Hierarchy \ClassHierarchy ;
17
18
use function array_key_exists ;
18
19
use function array_keys ;
19
20
use function array_merge ;
21
+ use function explode ;
20
22
use function in_array ;
21
23
use function strpos ;
22
24
26
28
class DeadMethodRule implements Rule
27
29
{
28
30
31
+ private const UNSUPPORTED_MAGIC_METHODS = [
32
+ '__invoke ' => null ,
33
+ '__toString ' => null ,
34
+ '__destruct ' => null ,
35
+ '__call ' => null ,
36
+ '__callStatic ' => null ,
37
+ '__get ' => null ,
38
+ '__set ' => null ,
39
+ '__isset ' => null ,
40
+ '__unset ' => null ,
41
+ '__sleep ' => null ,
42
+ '__wakeup ' => null ,
43
+ '__serialize ' => null ,
44
+ '__unserialize ' => null ,
45
+ '__set_state ' => null ,
46
+ '__debugInfo ' => null ,
47
+ ];
48
+
29
49
private ClassHierarchy $ classHierarchy ;
30
50
31
51
/**
@@ -35,7 +55,7 @@ class DeadMethodRule implements Rule
35
55
* kind: string,
36
56
* name: string,
37
57
* file: string,
38
- * methods: array<string, array{line: int, abstract: bool}>,
58
+ * methods: array<string, array{line: int, abstract: bool, visibility: int-mask-of<Visibility::*> }>,
39
59
* parents: array<string, null>,
40
60
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
41
61
* interfaces: array<string, null>
@@ -53,7 +73,7 @@ class DeadMethodRule implements Rule
53
73
/**
54
74
* @var array<string, array{string, int}> methodKey => [file, line]
55
75
*/
56
- private array $ deadMethods = [];
76
+ private array $ blackMethods = [];
57
77
58
78
/**
59
79
* @var array<string, list<string>> caller => callee[]
@@ -117,7 +137,7 @@ public function processNode(
117
137
118
138
foreach ($ methods as $ methodName => $ methodData ) {
119
139
$ definition = $ this ->getMethodKey ($ typeName , $ methodName );
120
- $ this ->deadMethods [$ definition ] = [$ file , $ methodData ['line ' ]];
140
+ $ this ->blackMethods [$ definition ] = [$ file , $ methodData ['line ' ]];
121
141
}
122
142
}
123
143
@@ -133,9 +153,7 @@ public function processNode(
133
153
$ callerKey = $ call ->caller === null || $ this ->isAnonymousClass ($ call ->caller ->className )
134
154
? ''
135
155
: $ call ->caller ->toString ();
136
- $ isWhite = $ call ->caller === null
137
- || $ this ->isAnonymousClass ($ call ->caller ->className )
138
- || Method::isUnsupported ($ call ->caller ->methodName );
156
+ $ isWhite = $ this ->isConsideredWhite ($ call );
139
157
140
158
foreach ($ this ->getAlternativeCalleeKeys ($ call ) as $ possibleCalleeKey ) {
141
159
$ this ->callGraph [$ callerKey ][] = $ possibleCalleeKey ;
@@ -160,18 +178,24 @@ public function processNode(
160
178
$ call = Call::fromString ($ entrypoint );
161
179
162
180
foreach ($ this ->getAlternativeCalleeKeys ($ call ) as $ methodDefinition ) {
163
- unset($ this ->deadMethods [$ methodDefinition ]);
181
+ unset($ this ->blackMethods [$ methodDefinition ]);
164
182
}
165
183
166
184
$ this ->markTransitiveCallsWhite ($ call ->callee ->toString ());
167
185
}
168
186
}
169
187
}
170
188
189
+ foreach ($ this ->blackMethods as $ blackMethodKey => $ _ ) {
190
+ if ($ this ->isNeverReportedAsDead ($ blackMethodKey )) {
191
+ unset($ this ->blackMethods [$ blackMethodKey ]);
192
+ }
193
+ }
194
+
171
195
$ errors = [];
172
196
173
197
if ($ this ->reportTransitivelyDeadMethodAsSeparateError ) {
174
- foreach ($ this ->deadMethods as $ deadMethodKey => [$ file , $ line ]) {
198
+ foreach ($ this ->blackMethods as $ deadMethodKey => [$ file , $ line ]) {
175
199
$ errors [] = $ this ->buildError ($ deadMethodKey , [], $ file , $ line );
176
200
}
177
201
@@ -181,11 +205,11 @@ public function processNode(
181
205
$ deadGroups = $ this ->groupDeadMethods ();
182
206
183
207
foreach ($ deadGroups as $ deadGroupKey => $ deadSubgroupKeys ) {
184
- [$ file , $ line ] = $ this ->deadMethods [$ deadGroupKey ]; // @phpstan-ignore offsetAccess.notFound
208
+ [$ file , $ line ] = $ this ->blackMethods [$ deadGroupKey ]; // @phpstan-ignore offsetAccess.notFound
185
209
$ subGroupMap = [];
186
210
187
211
foreach ($ deadSubgroupKeys as $ deadSubgroupKey ) {
188
- $ subGroupMap [$ deadSubgroupKey ] = $ this ->deadMethods [$ deadSubgroupKey ]; // @phpstan-ignore offsetAccess.notFound
212
+ $ subGroupMap [$ deadSubgroupKey ] = $ this ->blackMethods [$ deadSubgroupKey ]; // @phpstan-ignore offsetAccess.notFound
189
213
}
190
214
191
215
$ errors [] = $ this ->buildError ($ deadGroupKey , $ subGroupMap , $ file , $ line );
@@ -290,14 +314,14 @@ private function markTransitiveCallsWhite(string $callerKey, array $visitedKeys
290
314
$ visitedKeys = $ visitedKeys === [] ? [$ callerKey => null ] : $ visitedKeys ;
291
315
$ calleeKeys = $ this ->callGraph [$ callerKey ] ?? [];
292
316
293
- unset($ this ->deadMethods [$ callerKey ]);
317
+ unset($ this ->blackMethods [$ callerKey ]);
294
318
295
319
foreach ($ calleeKeys as $ calleeKey ) {
296
320
if (array_key_exists ($ calleeKey , $ visitedKeys )) {
297
321
continue ;
298
322
}
299
323
300
- if (!isset ($ this ->deadMethods [$ calleeKey ])) {
324
+ if (!isset ($ this ->blackMethods [$ calleeKey ])) {
301
325
continue ;
302
326
}
303
327
@@ -321,7 +345,7 @@ private function getTransitiveDeadCalls(string $callerKey, array $visitedKeys =
321
345
continue ;
322
346
}
323
347
324
- if (!isset ($ this ->deadMethods [$ calleeKey ])) {
348
+ if (!isset ($ this ->blackMethods [$ calleeKey ])) {
325
349
continue ;
326
350
}
327
351
@@ -346,20 +370,20 @@ private function groupDeadMethods(): array
346
370
$ deadMethodsWithCaller = [];
347
371
348
372
foreach ($ this ->callGraph as $ caller => $ callees ) {
349
- if (!array_key_exists ($ caller , $ this ->deadMethods )) {
373
+ if (!array_key_exists ($ caller , $ this ->blackMethods )) {
350
374
continue ;
351
375
}
352
376
353
377
foreach ($ callees as $ callee ) {
354
- if (array_key_exists ($ callee , $ this ->deadMethods )) {
378
+ if (array_key_exists ($ callee , $ this ->blackMethods )) {
355
379
$ deadMethodsWithCaller [$ callee ] = true ;
356
380
}
357
381
}
358
382
}
359
383
360
384
$ methodsGrouped = [];
361
385
362
- foreach ($ this ->deadMethods as $ deadMethodKey => $ _ ) {
386
+ foreach ($ this ->blackMethods as $ deadMethodKey => $ _ ) {
363
387
if (isset ($ methodsGrouped [$ deadMethodKey ])) {
364
388
continue ;
365
389
}
@@ -380,7 +404,7 @@ private function groupDeadMethods(): array
380
404
}
381
405
382
406
// now only cycles remain, lets pick group representatives based on first occurrence
383
- foreach ($ this ->deadMethods as $ deadMethodKey => $ _ ) {
407
+ foreach ($ this ->blackMethods as $ deadMethodKey => $ _ ) {
384
408
if (isset ($ methodsGrouped [$ deadMethodKey ])) {
385
409
continue ;
386
410
}
@@ -463,4 +487,37 @@ private function getMethodKey(string $typeName, string $methodName): string
463
487
return $ typeName . ':: ' . $ methodName ;
464
488
}
465
489
490
+ private function isConsideredWhite (Call $ call ): bool
491
+ {
492
+ return $ call ->caller === null
493
+ || $ this ->isAnonymousClass ($ call ->caller ->className )
494
+ || array_key_exists ($ call ->caller ->methodName , self ::UNSUPPORTED_MAGIC_METHODS );
495
+ }
496
+
497
+ private function isNeverReportedAsDead (string $ methodKey ): bool
498
+ {
499
+ [$ typeName , $ methodName ] = explode (':: ' , $ methodKey ); // @phpstan-ignore offsetAccess.notFound
500
+
501
+ $ kind = $ this ->typeDefinitions [$ typeName ]['kind ' ] ?? null ;
502
+ $ abstract = $ this ->typeDefinitions [$ typeName ]['methods ' ][$ methodName ]['abstract ' ] ?? false ;
503
+ $ visibility = $ this ->typeDefinitions [$ typeName ]['methods ' ][$ methodName ]['visibility ' ] ?? 0 ;
504
+
505
+ if ($ kind === Kind::TRAIT && $ abstract ) {
506
+ // abstract methods in traits make sense (not dead) only when called within the trait itself, but that is hard to detect for now, so lets ignore them completely
507
+ // the difference from interface methods (or abstract methods) is that those methods can be called over the interface, but you cannot call method over trait
508
+ return true ;
509
+ }
510
+
511
+ if ($ methodName === '__construct ' && ($ visibility & Visibility::PRIVATE ) !== 0 ) {
512
+ // private constructors are often used to deny instantiation
513
+ return true ;
514
+ }
515
+
516
+ if (array_key_exists ($ methodName , self ::UNSUPPORTED_MAGIC_METHODS )) {
517
+ return true ;
518
+ }
519
+
520
+ return false ;
521
+ }
522
+
466
523
}
0 commit comments