Skip to content

Commit 007c52f

Browse files
committed
Detect dead enum cases
1 parent 337b299 commit 007c52f

24 files changed

+329
-29
lines changed

src/Collector/ClassDefinitionCollector.php

+33-2
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>,
@@ -52,6 +54,7 @@ public function getNodeType(): string
5254
* @return array{
5355
* kind: string,
5456
* name: string,
57+
* cases: array<string, array{line: int}>,
5558
* constants: array<string, array{line: int}>,
5659
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
5760
* parents: array<string, null>,
@@ -73,6 +76,8 @@ public function processNode(
7376
$reflection = $this->reflectionProvider->getClass($typeName);
7477

7578
$methods = [];
79+
$constants = [];
80+
$cases = [];
7681

7782
foreach ($node->getMethods() as $method) {
7883
$methods[$method->name->toString()] = [
@@ -83,8 +88,6 @@ public function processNode(
8388
];
8489
}
8590

86-
$constants = [];
87-
8891
foreach ($node->getConstants() as $constant) {
8992
foreach ($constant->consts as $const) {
9093
$constants[$const->name->toString()] = [
@@ -93,10 +96,18 @@ public function processNode(
9396
}
9497
}
9598

99+
foreach ($this->getEnumCases($node) as $case) {
100+
$cases[$case->name->toString()] = [
101+
'line' => $case->name->getStartLine(),
102+
];
103+
104+
}
105+
96106
return [
97107
'kind' => $kind,
98108
'name' => $typeName,
99109
'methods' => $methods,
110+
'cases' => $cases,
100111
'constants' => $constants,
101112
'parents' => $this->getParents($reflection),
102113
'traits' => $this->getTraits($node),
@@ -182,4 +193,24 @@ private function getKind(ClassLike $node): string
182193
throw new LogicException('Unknown class-like node');
183194
}
184195

196+
/**
197+
* @return list<EnumCase>
198+
*/
199+
private function getEnumCases(ClassLike $node): array
200+
{
201+
if (!$node instanceof Enum_) {
202+
return [];
203+
}
204+
205+
$result = [];
206+
207+
foreach ($node->stmts as $stmt) {
208+
if ($stmt instanceof EnumCase) {
209+
$result[] = $stmt;
210+
}
211+
}
212+
213+
return $result;
214+
}
215+
185216
}

src/Collector/ConstantFetchCollector.php

+24-12
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
1818
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
1919
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
20+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
2021
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
22+
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseRef;
23+
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseUsage;
2124
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
2225
use function array_map;
2326
use function count;
@@ -145,14 +148,13 @@ private function registerFetch(ClassConstFetch $node, Scope $scope): void
145148
}
146149

147150
foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName, $possibleDescendantFetch) as $constantRef) {
148-
$this->registerUsage(
149-
new ClassConstantUsage(
150-
UsageOrigin::createRegular($node, $scope),
151-
$constantRef,
152-
),
153-
$node,
154-
$scope,
155-
);
151+
$origin = UsageOrigin::createRegular($node, $scope);
152+
153+
$usage = $constantRef instanceof EnumCaseRef
154+
? new EnumCaseUsage($origin, $constantRef)
155+
: new ClassConstantUsage($origin, $constantRef);
156+
157+
$this->registerUsage($usage, $node, $scope);
156158
}
157159
}
158160
}
@@ -178,7 +180,7 @@ private function getConstantNames(ClassConstFetch $fetch, Scope $scope): array
178180
}
179181

180182
/**
181-
* @return list<ClassConstantRef>
183+
* @return list<ClassConstantRef|EnumCaseRef>
182184
*/
183185
private function getDeclaringTypesWithConstant(
184186
Type $type,
@@ -190,20 +192,30 @@ private function getDeclaringTypesWithConstant(
190192
$classReflections = $typeNormalized->getObjectTypeOrClassStringObjectType()->getObjectClassReflections();
191193

192194
$result = [];
195+
$mayBeEnum = !$typeNormalized->isEnum()->no();
193196

194197
foreach ($classReflections as $classReflection) {
195198
$possibleDescendant = $isPossibleDescendant ?? !$classReflection->isFinal();
199+
200+
if ($mayBeEnum) {
201+
$result[] = new EnumCaseRef($classReflection->getName(), $constantName);
202+
}
203+
196204
$result[] = new ClassConstantRef($classReflection->getName(), $constantName, $possibleDescendant);
197205
}
198206

199-
if ($result === []) {
200-
$result[] = new ClassConstantRef(null, $constantName, true); // call over unknown type
207+
if ($result === []) { // call over unknown type
208+
$result[] = new ClassConstantRef(null, $constantName, true);
209+
$result[] = new EnumCaseRef(null, $constantName);
201210
}
202211

203212
return $result;
204213
}
205214

206-
private function registerUsage(ClassConstantUsage $usage, Node $node, Scope $scope): void
215+
/**
216+
* @param ClassConstantUsage|EnumCaseUsage $usage
217+
*/
218+
private function registerUsage(ClassMemberUsage $usage, Node $node, Scope $scope): void
207219
{
208220
$excluderName = null;
209221

src/Debug/DebugUsagePrinter.php

+4
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ private function printMixedEverythingUsages(Output $output, array $fullyMixedUsa
142142
}
143143

144144
foreach ($fullyMixedUsages as $memberType => $collectedUsages) {
145+
if ($memberType === MemberType::ENUM_CASE) {
146+
continue; // any fully mixed fetch is emitted twice (once as enum, once as const); report only once here
147+
}
148+
145149
$fullyMixedCount = count($collectedUsages);
146150

147151
$memberTypeString = $memberType === MemberType::METHOD ? 'method' : 'constant';

src/Enum/MemberType.php

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ interface MemberType
77

88
public const METHOD = 1;
99
public const CONSTANT = 2;
10+
public const ENUM_CASE = 3;
1011

1112
}

src/Formatter/RemoveDeadCodeFormatter.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function formatErrors(
5555
if (
5656
$fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_METHOD
5757
&& $fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_CONSTANT
58+
&& $fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_ENUM_CASE
5859
) {
5960
continue;
6061
}
@@ -77,10 +78,12 @@ public function formatErrors(
7778
$deadConstants = $deadMembersByType[MemberType::CONSTANT] ?? [];
7879
/** @var array<string, list<ClassMemberUsage>> $deadMethods */
7980
$deadMethods = $deadMembersByType[MemberType::METHOD] ?? [];
81+
/** @var array<string, list<ClassMemberUsage>> $deadEnumCases */
82+
$deadEnumCases = $deadMembersByType[MemberType::ENUM_CASE] ?? [];
8083

81-
$membersCount += count($deadConstants) + count($deadMethods);
84+
$membersCount += count($deadConstants) + count($deadMethods) + count($deadEnumCases);
8285

83-
$transformer = new RemoveDeadCodeTransformer(array_keys($deadMethods), array_keys($deadConstants));
86+
$transformer = new RemoveDeadCodeTransformer(array_keys($deadMethods), array_keys($deadConstants), array_keys($deadEnumCases));
8487
$oldCode = $this->fileSystem->read($file);
8588
$newCode = $transformer->transformCode($oldCode);
8689
$this->fileSystem->write($file, $newCode);
@@ -94,6 +97,11 @@ public function formatErrors(
9497
$output->writeLineFormatted(" • Removed method <fg=white>$method</>");
9598
$this->printExcludedUsages($output, $excludedUsages);
9699
}
100+
101+
foreach ($deadEnumCases as $case => $excludedUsages) {
102+
$output->writeLineFormatted(" • Removed enum case <fg=white>$case</>");
103+
$this->printExcludedUsages($output, $excludedUsages);
104+
}
97105
}
98106

99107
$memberPlural = $membersCount === 1 ? '' : 's';

src/Graph/ClassConstantRef.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ final class ClassConstantRef extends ClassMemberRef
1212

1313
public function __construct(
1414
?string $className,
15-
?string $constantName,
15+
?string $enumCaseName,
1616
bool $possibleDescendant
1717
)
1818
{
19-
parent::__construct($className, $constantName, $possibleDescendant);
19+
parent::__construct($className, $enumCaseName, $possibleDescendant);
2020
}
2121

2222
public static function buildKey(string $typeName, string $memberName): string

src/Graph/CollectedUsage.php

+13-4
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,24 @@ public static function deserialize(string $data, string $scopeFile): self
109109
);
110110
$exclusionReason = $result['e'];
111111

112-
$usage = $memberType === MemberType::CONSTANT
113-
? new ClassConstantUsage(
112+
if ($memberType === MemberType::CONSTANT) {
113+
$usage = new ClassConstantUsage(
114114
$origin,
115115
new ClassConstantRef($result['m']['c'], $result['m']['m'], $result['m']['d']),
116-
)
117-
: new ClassMethodUsage(
116+
);
117+
} elseif ($memberType === MemberType::METHOD) {
118+
$usage = new ClassMethodUsage(
118119
$origin,
119120
new ClassMethodRef($result['m']['c'], $result['m']['m'], $result['m']['d']),
120121
);
122+
} elseif ($memberType === MemberType::ENUM_CASE) {
123+
$usage = new EnumCaseUsage(
124+
$origin,
125+
new EnumCaseRef($result['m']['c'], $result['m']['m']),
126+
);
127+
} else {
128+
throw new LogicException('Unknown member type: ' . $memberType);
129+
}
121130

122131
return new self($usage, $exclusionReason);
123132
}

src/Graph/EnumCaseRef.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Graph;
4+
5+
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
6+
7+
/**
8+
* @immutable
9+
*/
10+
final class EnumCaseRef extends ClassMemberRef
11+
{
12+
13+
public function __construct(
14+
?string $className,
15+
?string $enumCaseName
16+
)
17+
{
18+
parent::__construct($className, $enumCaseName, false);
19+
}
20+
21+
public static function buildKey(string $typeName, string $memberName): string
22+
{
23+
return 'e/' . $typeName . '::' . $memberName;
24+
}
25+
26+
/**
27+
* @return MemberType::ENUM_CASE
28+
*/
29+
public function getMemberType(): int
30+
{
31+
return MemberType::ENUM_CASE;
32+
}
33+
34+
}

src/Graph/EnumCaseUsage.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Graph;
4+
5+
use LogicException;
6+
use ShipMonk\PHPStan\DeadCode\Enum\MemberType;
7+
8+
/**
9+
* @immutable
10+
*/
11+
final class EnumCaseUsage extends ClassMemberUsage
12+
{
13+
14+
private EnumCaseRef $enumCase;
15+
16+
/**
17+
* @param UsageOrigin $origin The method where the fetch occurs
18+
* @param EnumCaseRef $enumCase The case being fetched
19+
*/
20+
public function __construct(
21+
UsageOrigin $origin,
22+
EnumCaseRef $enumCase
23+
)
24+
{
25+
parent::__construct($origin);
26+
27+
$this->enumCase = $enumCase;
28+
}
29+
30+
/**
31+
* @return MemberType::ENUM_CASE
32+
*/
33+
public function getMemberType(): int
34+
{
35+
return MemberType::ENUM_CASE;
36+
}
37+
38+
public function getMemberRef(): EnumCaseRef
39+
{
40+
return $this->enumCase;
41+
}
42+
43+
public function concretizeMixedClassNameUsage(string $className): self
44+
{
45+
if ($this->enumCase->getClassName() !== null) {
46+
throw new LogicException('Usage is not mixed, thus it cannot be concretized');
47+
}
48+
49+
return new self(
50+
$this->getOrigin(),
51+
new EnumCaseRef(
52+
$className,
53+
$this->enumCase->getMemberName(),
54+
),
55+
);
56+
}
57+
58+
}

0 commit comments

Comments
 (0)