Skip to content

Commit 87441fe

Browse files
committed
EnumUsageProvider
1 parent 50b5322 commit 87441fe

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ parameters:
9797
- Any overridden method that originates in `vendor` is not reported as dead
9898
- e.g. implementing `Psr\Log\LoggerInterface::log` is automatically considered used
9999

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

102105
## Excluding usages in tests:

rules.neon

Lines changed: 12 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:
@@ -147,6 +154,8 @@ parameters:
147154
usageProviders:
148155
apiPhpDoc:
149156
enabled: true
157+
enum:
158+
enabled: true
150159
vendor:
151160
enabled: true
152161
reflection:
@@ -181,6 +190,9 @@ parametersSchema:
181190
apiPhpDoc: structure([
182191
enabled: bool()
183192
])
193+
enum: structure([
194+
enabled: bool()
195+
])
184196
vendor: structure([
185197
enabled: bool()
186198
])

src/Provider/EnumUsageProvider.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Provider;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Type\MixedType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
use PHPStan\Type\TypeUtils;
13+
use ReflectionEnum;
14+
use ReflectionEnumBackedCase;
15+
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseRef;
16+
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseUsage;
17+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
18+
use UnitEnum;
19+
use function array_filter;
20+
use function in_array;
21+
use function is_int;
22+
use function is_string;
23+
24+
class EnumUsageProvider implements MemberUsageProvider
25+
{
26+
27+
private bool $enabled;
28+
29+
public function __construct(bool $enabled)
30+
{
31+
$this->enabled = $enabled;
32+
}
33+
34+
public function getUsages(Node $node, Scope $scope): array
35+
{
36+
if ($this->enabled === false) {
37+
return [];
38+
}
39+
40+
if ($node instanceof StaticCall) {
41+
return $this->getTryFromUsages($node, $scope);
42+
}
43+
44+
return [];
45+
}
46+
47+
/**
48+
* @return list<EnumCaseUsage>
49+
*/
50+
private function getTryFromUsages(StaticCall $staticCall, Scope $scope): array
51+
{
52+
$methodNames = $this->getMethodNames($staticCall, $scope);
53+
$firstArgType = $this->getArgType($staticCall, $scope, 0);
54+
55+
$callerType = $staticCall->class instanceof Expr
56+
? $scope->getType($staticCall->class)
57+
: $scope->resolveTypeByName($staticCall->class);
58+
59+
$typeNoNull = TypeCombinator::removeNull($callerType); // remove null to support nullsafe calls
60+
$typeNormalized = TypeUtils::toBenevolentUnion($typeNoNull); // extract possible calls even from Class|int
61+
$classReflections = $typeNormalized->getObjectTypeOrClassStringObjectType()->getObjectClassReflections();
62+
63+
$result = [];
64+
65+
foreach ($methodNames as $methodName) {
66+
if (!in_array($methodName, ['tryFrom', 'from', 'cases'], true)) {
67+
continue;
68+
}
69+
70+
foreach ($classReflections as $classReflection) {
71+
if (!$classReflection->isEnum()) {
72+
continue;
73+
}
74+
75+
$valueToCaseMapping = $this->getValueToEnumCaseMapping($classReflection->getNativeReflection()); // @phpstan-ignore argument.type (https://github.com/phpstan/phpstan-src/pull/3925)
76+
$triedValues = $firstArgType->getConstantScalarValues() === []
77+
? [null]
78+
: array_filter($firstArgType->getConstantScalarValues(), static fn($value): bool => is_string($value) || is_int($value));
79+
80+
foreach ($triedValues as $value) {
81+
$enumCase = $value === null ? null : $valueToCaseMapping[$value] ?? null;
82+
$result[] = new EnumCaseUsage(
83+
UsageOrigin::createRegular($staticCall, $scope),
84+
new EnumCaseRef($classReflection->getName(), $enumCase),
85+
);
86+
}
87+
}
88+
}
89+
90+
return $result;
91+
}
92+
93+
/**
94+
* @return list<string|null>
95+
*/
96+
private function getMethodNames(StaticCall $call, Scope $scope): array
97+
{
98+
if ($call->name instanceof Expr) {
99+
$possibleMethodNames = [];
100+
101+
foreach ($scope->getType($call->name)->getConstantStrings() as $constantString) {
102+
$possibleMethodNames[] = $constantString->getValue();
103+
}
104+
105+
return $possibleMethodNames === []
106+
? [null] // unknown method name
107+
: $possibleMethodNames;
108+
}
109+
110+
return [$call->name->name];
111+
}
112+
113+
private function getArgType(StaticCall $staticCall, Scope $scope, int $position): Type
114+
{
115+
$args = $staticCall->getArgs();
116+
117+
if (isset($args[$position])) {
118+
return $scope->getType($args[$position]->value);
119+
}
120+
121+
return new MixedType();
122+
}
123+
124+
/**
125+
* @param ReflectionEnum<UnitEnum> $enumReflection
126+
* @return array<array-key, string>
127+
*/
128+
private function getValueToEnumCaseMapping(ReflectionEnum $enumReflection): array
129+
{
130+
$mapping = [];
131+
132+
foreach ($enumReflection->getCases() as $enumCaseReflection) {
133+
if (!$enumCaseReflection instanceof ReflectionEnumBackedCase) {
134+
continue;
135+
}
136+
137+
$mapping[$enumCaseReflection->getBackingValue()] = $enumCaseReflection->getName();
138+
}
139+
140+
return $mapping;
141+
}
142+
143+
}

tests/Rule/DeadCodeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use ShipMonk\PHPStan\DeadCode\Output\OutputEnhancer;
3737
use ShipMonk\PHPStan\DeadCode\Provider\ApiPhpDocUsageProvider;
3838
use ShipMonk\PHPStan\DeadCode\Provider\DoctrineUsageProvider;
39+
use ShipMonk\PHPStan\DeadCode\Provider\EnumUsageProvider;
3940
use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider;
4041
use ShipMonk\PHPStan\DeadCode\Provider\NetteUsageProvider;
4142
use ShipMonk\PHPStan\DeadCode\Provider\PhpStanUsageProvider;
@@ -764,6 +765,7 @@ public static function provideFiles(): Traversable
764765
yield 'provider-phpstan' => [__DIR__ . '/data/providers/phpstan.php'];
765766
yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php'];
766767
yield 'provider-apiphpdoc' => [__DIR__ . '/data/providers/api-phpdoc.php'];
768+
yield 'provider-enum' => [__DIR__ . '/data/providers/enum.php', self::requiresPhp(8_01_00)];
767769

768770
// excluders
769771
yield 'excluder-tests' => [[__DIR__ . '/data/excluders/tests/src/code.php', __DIR__ . '/data/excluders/tests/tests/code.php']];
@@ -912,6 +914,9 @@ private function getMemberUsageProviders(): array
912914
self::createReflectionProvider(),
913915
$this->providersEnabled,
914916
),
917+
new EnumUsageProvider(
918+
true,
919+
),
915920
];
916921

917922
if ($this->providersEnabled) {

tests/Rule/data/providers/enum.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace EnumProvider;
4+
5+
use Nette\Application\UI\Form as UIForm;
6+
use Nette\Application\UI\Presenter;
7+
use Nette\SmartObject;
8+
9+
enum MyEnum0: int {
10+
case One = 1;
11+
case Two = 2; // error: Unused EnumProvider\MyEnum0::Two
12+
}
13+
14+
enum MyEnum1: string {
15+
case Used = 'used';
16+
case Unused = 'unused'; // error: Unused EnumProvider\MyEnum1::Unused
17+
}
18+
19+
enum MyEnum2: string {
20+
case Used = 'used';
21+
case UsedToo = 'used_too';
22+
}
23+
24+
enum MyEnum3: string {
25+
case Used = 'used';
26+
case UsedToo = 'used_too';
27+
}
28+
29+
function test(string $any) {
30+
MyEnum0::tryFrom(1);
31+
MyEnum1::tryFrom('used');
32+
MyEnum2::from($any);
33+
MyEnum3::cases();
34+
}

0 commit comments

Comments
 (0)