Skip to content

Commit 58ef3d3

Browse files
authored
Detect dead enum cases (#197)
1 parent 4cd775d commit 58ef3d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1333
-231
lines changed

.github/workflows/checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
strategy:
5656
fail-fast: false
5757
matrix:
58-
php-version: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ]
58+
php-version: [ '8.0', '8.1', '8.2', '8.3', '8.4' ]
5959
steps:
6060
-
6161
name: Checkout code

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ $ vendor/bin/phpstan
5858
- `Doctrine\Common\EventSubscriber` methods
5959
- `repositoryMethod` in `#[UniqueEntity]` attribute
6060
- lifecycle event attributes `#[PreFlush]`, `#[PostLoad]`, ...
61+
- enums in `#[Column(enumType: UserStatus::class)]`
6162

6263
#### PHPUnit:
6364
- **data provider methods**
@@ -90,13 +91,16 @@ parameters:
9091
## Generic usage providers:
9192

9293
#### Reflection:
93-
- Any constant or method accessed via `ReflectionClass` is detected as used
94-
- e.g. `$reflection->getConstructor()`, `$reflection->getConstant('NAME')`, `$reflection->getMethods()`, ...
94+
- Any enum, constant or method accessed via `ReflectionClass` is detected as used
95+
- e.g. `$reflection->getConstructor()`, `$reflection->getConstant('NAME')`, `$reflection->getMethods()`, `$reflection->getCases()`...
9596

9697
#### Vendor:
9798
- Any overridden method that originates in `vendor` is not reported as dead
9899
- e.g. implementing `Psr\Log\LoggerInterface::log` is automatically considered used
99100

101+
#### Enum:
102+
- Detects usages caused by `BackedEnum::from`, `BackedEnum::tryFrom` and `UnitEnum::cases`
103+
100104
Those providers are enabled by default, but you can disable them if needed.
101105

102106
## Excluding usages in tests:
@@ -340,6 +344,21 @@ $methods = $reflection->getMethods(); // all Foo methods are used here
340344

341345
- All that applies even to constant fetches (e.g. `Foo::{$unknown}`)
342346

347+
## Detected class members:
348+
Default configuration is:
349+
350+
```neon
351+
parameters:
352+
shipmonkDeadCode:
353+
detect:
354+
deadMethods: true
355+
deadConstants: true
356+
deadEnumCases: false
357+
```
358+
359+
Enum cases are disabled by default as those are often used in API input objects (using custom deserialization, which typically require custom usage provider).
360+
361+
343362
## Comparison with tomasvotruba/unused-public
344363
- You can see [detailed comparison PR](https://github.com/shipmonk-rnd/dead-code-detector/pull/53)
345364
- Basically, their analysis is less precise and less flexible. Mainly:

composer-dependency-analyser.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@
88

99
require_once('phar://phpstan.phar/preload.php'); // prepends PHPStan's PharAutolaoder to composer's autoloader
1010

11-
return (new Configuration())
11+
$config = (new Configuration())
1212
->ignoreErrorsOnPath(__DIR__ . '/src/Provider', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // providers are designed that way
1313
->ignoreErrorsOnExtensionAndPath('ext-simplexml', __DIR__ . '/src/Provider/SymfonyUsageProvider.php', [ErrorType::SHADOW_DEPENDENCY]) // guarded with extension_loaded()
1414
->addPathToExclude(__DIR__ . '/tests/Rule/data');
15+
16+
if (PHP_VERSION_ID < 80100) {
17+
$config->ignoreUnknownClasses([
18+
'ReflectionEnum',
19+
'ReflectionEnumBackedCase',
20+
'ReflectionEnumUnitCase'
21+
]);
22+
}
23+
24+
return $config;

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
],
1414
"require": {
1515
"php": "^7.4 || ^8.0",
16-
"phpstan/phpstan": "^2.1.9"
16+
"phpstan/phpstan": "^2.1.12"
1717
},
1818
"require-dev": {
1919
"composer-runtime-api": "^2.0",

composer.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpstan.neon.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ parameters:
5555
message: "#but it's missing from the PHPDoc @throws tag\\.$#" # allow uncatched exceptions in tests
5656
path: tests/*
5757

58+
-
59+
message: "#class ReflectionEnum is not generic#"
60+
reportUnmatched: false # reported only with PHP 8.0
61+
5862
# allow referencing any attribute classes
5963
- '#^Parameter \#1 \$name of method PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass\:\:getAttributes\(\) expects class\-string\|null, string given\.$#'
6064
- '#^Parameter \#1 \$name of method PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod\:\:getAttributes\(\) expects class\-string\|null, string given\.$#'

rules.neon

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ services:
2121
arguments:
2222
enabled: %shipmonkDeadCode.usageProviders.apiPhpDoc.enabled%
2323

24+
-
25+
class: ShipMonk\PHPStan\DeadCode\Provider\EnumUsageProvider
26+
tags:
27+
- shipmonk.deadCode.memberUsageProvider
28+
arguments:
29+
enabled: %shipmonkDeadCode.usageProviders.enum.enabled%
30+
2431
-
2532
class: ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider
2633
tags:
@@ -113,6 +120,10 @@ services:
113120
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
114121
tags:
115122
- phpstan.collector
123+
arguments:
124+
detectDeadMethods: %shipmonkDeadCode.detect.deadMethods%
125+
detectDeadConstants: %shipmonkDeadCode.detect.deadConstants%
126+
detectDeadEnumCases: %shipmonkDeadCode.detect.deadEnumCases%
116127

117128
-
118129
class: ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector
@@ -144,9 +155,15 @@ parameters:
144155
shipmonkDeadCode:
145156
trackMixedAccess: null
146157
reportTransitivelyDeadMethodAsSeparateError: false
158+
detect:
159+
deadMethods: true
160+
deadConstants: true
161+
deadEnumCases: false
147162
usageProviders:
148163
apiPhpDoc:
149164
enabled: true
165+
enum:
166+
enabled: true
150167
vendor:
151168
enabled: true
152169
reflection:
@@ -177,10 +194,18 @@ parametersSchema:
177194
shipmonkDeadCode: structure([
178195
trackMixedAccess: schema(bool(), nullable()) # deprecated, use usageExcluders.usageOverMixed.enabled
179196
reportTransitivelyDeadMethodAsSeparateError: bool()
197+
detect: structure([
198+
deadMethods: bool()
199+
deadConstants: bool()
200+
deadEnumCases: bool()
201+
])
180202
usageProviders: structure([
181203
apiPhpDoc: structure([
182204
enabled: bool()
183205
])
206+
enum: structure([
207+
enabled: bool()
208+
])
184209
vendor: structure([
185210
enabled: bool()
186211
])

src/Collector/ClassDefinitionCollector.php

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\Node\Stmt\Class_;
88
use PhpParser\Node\Stmt\ClassLike;
99
use PhpParser\Node\Stmt\Enum_;
10+
use PhpParser\Node\Stmt\EnumCase;
1011
use PhpParser\Node\Stmt\Interface_;
1112
use PhpParser\Node\Stmt\Trait_;
1213
use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
@@ -25,6 +26,7 @@
2526
* @implements Collector<ClassLike, array{
2627
* kind: string,
2728
* name: string,
29+
* cases: array<string, array{line: int}>,
2830
* constants: array<string, array{line: int}>,
2931
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
3032
* parents: array<string, null>,
@@ -37,9 +39,23 @@ class ClassDefinitionCollector implements Collector
3739

3840
private ReflectionProvider $reflectionProvider;
3941

40-
public function __construct(ReflectionProvider $reflectionProvider)
42+
private bool $detectDeadMethods;
43+
44+
private bool $detectDeadConstants;
45+
46+
private bool $detectDeadEnumCases;
47+
48+
public function __construct(
49+
ReflectionProvider $reflectionProvider,
50+
bool $detectDeadMethods,
51+
bool $detectDeadConstants,
52+
bool $detectDeadEnumCases
53+
)
4154
{
4255
$this->reflectionProvider = $reflectionProvider;
56+
$this->detectDeadMethods = $detectDeadMethods;
57+
$this->detectDeadConstants = $detectDeadConstants;
58+
$this->detectDeadEnumCases = $detectDeadEnumCases;
4359
}
4460

4561
public function getNodeType(): string
@@ -52,6 +68,7 @@ public function getNodeType(): string
5268
* @return array{
5369
* kind: string,
5470
* name: string,
71+
* cases: array<string, array{line: int}>,
5572
* constants: array<string, array{line: int}>,
5673
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
5774
* parents: array<string, null>,
@@ -73,22 +90,34 @@ public function processNode(
7390
$reflection = $this->reflectionProvider->getClass($typeName);
7491

7592
$methods = [];
76-
77-
foreach ($node->getMethods() as $method) {
78-
$methods[$method->name->toString()] = [
79-
'line' => $method->name->getStartLine(),
80-
'params' => count($method->params),
81-
'abstract' => $method->isAbstract() || $node instanceof Interface_,
82-
'visibility' => $method->flags & (Visibility::PUBLIC | Visibility::PROTECTED | Visibility::PRIVATE),
83-
];
93+
$constants = [];
94+
$cases = [];
95+
96+
if ($this->detectDeadMethods) {
97+
foreach ($node->getMethods() as $method) {
98+
$methods[$method->name->toString()] = [
99+
'line' => $method->name->getStartLine(),
100+
'params' => count($method->params),
101+
'abstract' => $method->isAbstract() || $node instanceof Interface_,
102+
'visibility' => $method->flags & (Visibility::PUBLIC | Visibility::PROTECTED | Visibility::PRIVATE),
103+
];
104+
}
84105
}
85106

86-
$constants = [];
107+
if ($this->detectDeadConstants) {
108+
foreach ($node->getConstants() as $constant) {
109+
foreach ($constant->consts as $const) {
110+
$constants[$const->name->toString()] = [
111+
'line' => $const->getStartLine(),
112+
];
113+
}
114+
}
115+
}
87116

88-
foreach ($node->getConstants() as $constant) {
89-
foreach ($constant->consts as $const) {
90-
$constants[$const->name->toString()] = [
91-
'line' => $const->getStartLine(),
117+
if ($this->detectDeadEnumCases) {
118+
foreach ($this->getEnumCases($node) as $case) {
119+
$cases[$case->name->toString()] = [
120+
'line' => $case->name->getStartLine(),
92121
];
93122
}
94123
}
@@ -97,6 +126,7 @@ public function processNode(
97126
'kind' => $kind,
98127
'name' => $typeName,
99128
'methods' => $methods,
129+
'cases' => $cases,
100130
'constants' => $constants,
101131
'parents' => $this->getParents($reflection),
102132
'traits' => $this->getTraits($node),
@@ -182,4 +212,24 @@ private function getKind(ClassLike $node): string
182212
throw new LogicException('Unknown class-like node');
183213
}
184214

215+
/**
216+
* @return list<EnumCase>
217+
*/
218+
private function getEnumCases(ClassLike $node): array
219+
{
220+
if (!$node instanceof Enum_) {
221+
return [];
222+
}
223+
224+
$result = [];
225+
226+
foreach ($node->stmts as $stmt) {
227+
if ($stmt instanceof EnumCase) {
228+
$result[] = $stmt;
229+
}
230+
}
231+
232+
return $result;
233+
}
234+
185235
}

src/Collector/ConstantFetchCollector.php

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Analyser\Scope;
1212
use PHPStan\Collectors\Collector;
1313
use PHPStan\Reflection\ReflectionProvider;
14+
use PHPStan\TrinaryLogic;
1415
use PHPStan\Type\Constant\ConstantStringType;
1516
use PHPStan\Type\Type;
1617
use PHPStan\Type\TypeUtils;
@@ -121,7 +122,7 @@ private function registerFunctionCall(
121122
$this->registerUsage(
122123
new ClassConstantUsage(
123124
UsageOrigin::createRegular($node, $scope),
124-
new ClassConstantRef($className, $constantName, true),
125+
new ClassConstantRef($className, $constantName, true, TrinaryLogic::createMaybe()),
125126
),
126127
$node,
127128
$scope,
@@ -151,14 +152,10 @@ private function registerFetch(
151152
}
152153

153154
foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName, $possibleDescendantFetch) as $constantRef) {
154-
$this->registerUsage(
155-
new ClassConstantUsage(
156-
UsageOrigin::createRegular($node, $scope),
157-
$constantRef,
158-
),
159-
$node,
160-
$scope,
161-
);
155+
$origin = UsageOrigin::createRegular($node, $scope);
156+
$usage = new ClassConstantUsage($origin, $constantRef);
157+
158+
$this->registerUsage($usage, $node, $scope);
162159
}
163160
}
164161
}
@@ -187,26 +184,33 @@ private function getConstantNames(
187184
}
188185

189186
/**
190-
* @return list<ClassConstantRef>
187+
* @return list<ClassConstantRef<?string, ?string>>
191188
*/
192189
private function getDeclaringTypesWithConstant(
193190
Type $type,
194191
?string $constantName,
195192
?bool $isPossibleDescendant
196193
): array
197194
{
198-
$typeNormalized = TypeUtils::toBenevolentUnion($type); // extract possible fetches even from Class|int
199-
$classReflections = $typeNormalized->getObjectTypeOrClassStringObjectType()->getObjectClassReflections();
195+
$typeNormalized = TypeUtils::toBenevolentUnion($type) // extract possible fetches even from Class|int
196+
->getObjectTypeOrClassStringObjectType();
197+
$classReflections = $typeNormalized->getObjectClassReflections();
200198

201199
$result = [];
200+
$isEnumCaseFetch = $typeNormalized->isEnum()->no() ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe();
202201

203202
foreach ($classReflections as $classReflection) {
204203
$possibleDescendant = $isPossibleDescendant ?? !$classReflection->isFinal();
205-
$result[] = new ClassConstantRef($classReflection->getName(), $constantName, $possibleDescendant);
204+
$result[] = new ClassConstantRef(
205+
$classReflection->getName(),
206+
$constantName,
207+
$possibleDescendant,
208+
$isEnumCaseFetch,
209+
);
206210
}
207211

208-
if ($result === []) {
209-
$result[] = new ClassConstantRef(null, $constantName, true); // call over unknown type
212+
if ($result === []) { // call over unknown type
213+
$result[] = new ClassConstantRef(null, $constantName, true, $isEnumCaseFetch);
210214
}
211215

212216
return $result;

src/Collector/MethodCallCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ private function getMethodNames(
257257
}
258258

259259
/**
260-
* @return list<ClassMethodRef>
260+
* @return list<ClassMethodRef<?string, ?string>>
261261
*/
262262
private function getDeclaringTypesWithMethod(
263263
?string $methodName,

0 commit comments

Comments
 (0)