Skip to content

Commit 49efe23

Browse files
authored
Symfony: Mark enum cases as used in invokable command parameters (#343)
1 parent 48ba42f commit 49efe23

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

src/Provider/SymfonyUsageProvider.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
use RecursiveDirectoryIterator;
3131
use RecursiveIteratorIterator;
3232
use ReflectionAttribute;
33+
use ReflectionEnum;
34+
use ReflectionNamedType;
3335
use Reflector;
3436
use ShipMonk\PHPStan\DeadCode\Enum\AccessType;
3537
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
@@ -152,6 +154,7 @@ public function getUsages(
152154
...$this->getMethodUsagesFromReflection($node),
153155
...$this->getPropertyUsagesFromReflection($node),
154156
...$this->getConstantUsages($node->getClassReflection()),
157+
...$this->getInvokableCommandEnumUsages($node),
155158
];
156159
}
157160

@@ -1486,6 +1489,81 @@ private function getConstantUsages(ClassReflection $classReflection): array
14861489
return $usages;
14871490
}
14881491

1492+
/**
1493+
* @return list<ClassConstantUsage>
1494+
*/
1495+
private function getInvokableCommandEnumUsages(InClassNode $node): array
1496+
{
1497+
$classReflection = $node->getClassReflection();
1498+
$nativeReflection = $classReflection->getNativeReflection();
1499+
1500+
if ($nativeReflection instanceof ReflectionEnum) {
1501+
return [];
1502+
}
1503+
1504+
$isCommand = $this->hasAttribute($nativeReflection, 'Symfony\Component\Console\Attribute\AsCommand')
1505+
|| $nativeReflection->isSubclassOf('Symfony\Component\Console\Command\Command');
1506+
1507+
if (!$isCommand) {
1508+
return [];
1509+
}
1510+
1511+
$invokeMethod = null;
1512+
1513+
foreach ($nativeReflection->getMethods() as $method) {
1514+
if ($method->getName() === '__invoke') {
1515+
$invokeMethod = $method;
1516+
break;
1517+
}
1518+
}
1519+
1520+
if ($invokeMethod === null) {
1521+
return [];
1522+
}
1523+
1524+
$usages = [];
1525+
1526+
foreach ($invokeMethod->getParameters() as $parameter) {
1527+
$type = $parameter->getType();
1528+
1529+
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
1530+
continue;
1531+
}
1532+
1533+
$typeName = $type->getName();
1534+
1535+
if (!$this->reflectionProvider->hasClass($typeName)) {
1536+
continue;
1537+
}
1538+
1539+
$enumReflection = $this->reflectionProvider->getClass($typeName);
1540+
1541+
if (!$enumReflection->isBackedEnum()) {
1542+
continue;
1543+
}
1544+
1545+
$nativeEnumReflection = $enumReflection->getNativeReflection();
1546+
1547+
if (!$nativeEnumReflection instanceof ReflectionEnum) {
1548+
continue;
1549+
}
1550+
1551+
foreach ($nativeEnumReflection->getCases() as $case) {
1552+
$usages[] = new ClassConstantUsage(
1553+
UsageOrigin::createVirtual($this, VirtualUsageData::withNote('Invokable command parameter in ' . $nativeReflection->getShortName())),
1554+
new ClassConstantRef(
1555+
$typeName,
1556+
$case->getName(),
1557+
possibleDescendant: false,
1558+
isEnumCase: TrinaryLogic::createYes(),
1559+
),
1560+
);
1561+
}
1562+
}
1563+
1564+
return $usages;
1565+
}
1566+
14891567
private function getContainerXmlPath(Container $container): ?string
14901568
{
14911569
try {

tests/Rule/data/providers/symfony.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,39 @@ class HelloCommand
101101
public function __construct() {}
102102
}
103103

104+
enum InvokableCommandMode: string {
105+
case Dry = 'dry';
106+
case Wet = 'wet';
107+
}
108+
109+
enum InvokableCommandUnused: string {
110+
case A = 'a';
111+
case B = 'b'; // error: Unused Symfony\InvokableCommandUnused::B
112+
}
113+
114+
enum InvokableCommandModeViaExtend: string {
115+
case Fast = 'fast';
116+
case Slow = 'slow';
117+
}
118+
119+
#[AsCommand('app:invokable')]
120+
class InvokableCommand
121+
{
122+
public function __invoke(InvokableCommandMode $mode): int
123+
{
124+
InvokableCommandUnused::A;
125+
return 0;
126+
}
127+
}
128+
129+
class InvokableCommandViaExtend extends Command
130+
{
131+
public function __invoke(InvokableCommandModeViaExtend $mode): int
132+
{
133+
return 0;
134+
}
135+
}
136+
104137
class DicClassParent { // not present in DIC, but ctor is not dead
105138
public function __construct() {}
106139
}

0 commit comments

Comments
 (0)