Skip to content

Detect dead enum cases #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ parameters:
## Generic usage providers:

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

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

#### Enum:
- Detects usages caused by `BackedEnum::from`, `BackedEnum::tryFrom` and `UnitEnum::cases`

Those providers are enabled by default, but you can disable them if needed.

## Excluding usages in tests:
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.9"
"phpstan/phpstan": "^2.1.12"
},
"require-dev": {
"composer-runtime-api": "^2.0",
Expand Down
12 changes: 6 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ services:
-
class: ShipMonk\PHPStan\DeadCode\Debug\DebugUsagePrinter

-
class: ShipMonk\PHPStan\DeadCode\Provider\EnumUsageProvider
tags:
- shipmonk.deadCode.memberUsageProvider
arguments:
enabled: %shipmonkDeadCode.usageProviders.enum.enabled%

-
class: ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider
tags:
Expand Down Expand Up @@ -136,6 +143,8 @@ parameters:
trackMixedAccess: null
reportTransitivelyDeadMethodAsSeparateError: false
usageProviders:
enum:
enabled: true
vendor:
enabled: true
reflection:
Expand Down Expand Up @@ -167,6 +176,9 @@ parametersSchema:
trackMixedAccess: schema(bool(), nullable()) # deprecated, use usageExcluders.usageOverMixed.enabled
reportTransitivelyDeadMethodAsSeparateError: bool()
usageProviders: structure([
enum: structure([
enabled: bool()
])
vendor: structure([
enabled: bool()
])
Expand Down
35 changes: 33 additions & 2 deletions src/Collector/ClassDefinitionCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\EnumCase;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\TraitUseAdaptation\Alias;
Expand All @@ -25,6 +26,7 @@
* @implements Collector<ClassLike, array{
* kind: string,
* name: string,
* cases: array<string, array{line: int}>,
* constants: array<string, array{line: int}>,
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
Expand Down Expand Up @@ -52,6 +54,7 @@ public function getNodeType(): string
* @return array{
* kind: string,
* name: string,
* cases: array<string, array{line: int}>,
* constants: array<string, array{line: int}>,
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
Expand All @@ -73,6 +76,8 @@ public function processNode(
$reflection = $this->reflectionProvider->getClass($typeName);

$methods = [];
$constants = [];
$cases = [];

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

$constants = [];

foreach ($node->getConstants() as $constant) {
foreach ($constant->consts as $const) {
$constants[$const->name->toString()] = [
Expand All @@ -93,10 +96,18 @@ public function processNode(
}
}

foreach ($this->getEnumCases($node) as $case) {
$cases[$case->name->toString()] = [
'line' => $case->name->getStartLine(),
];

}

return [
'kind' => $kind,
'name' => $typeName,
'methods' => $methods,
'cases' => $cases,
'constants' => $constants,
'parents' => $this->getParents($reflection),
'traits' => $this->getTraits($node),
Expand Down Expand Up @@ -182,4 +193,24 @@ private function getKind(ClassLike $node): string
throw new LogicException('Unknown class-like node');
}

/**
* @return list<EnumCase>
*/
private function getEnumCases(ClassLike $node): array
{
if (!$node instanceof Enum_) {
return [];
}

$result = [];

foreach ($node->stmts as $stmt) {
if ($stmt instanceof EnumCase) {
$result[] = $stmt;
}
}

return $result;
}

}
36 changes: 24 additions & 12 deletions src/Collector/ConstantFetchCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseRef;
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function array_map;
use function count;
Expand Down Expand Up @@ -145,14 +148,13 @@ private function registerFetch(ClassConstFetch $node, Scope $scope): void
}

foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName, $possibleDescendantFetch) as $constantRef) {
$this->registerUsage(
new ClassConstantUsage(
UsageOrigin::createRegular($node, $scope),
$constantRef,
),
$node,
$scope,
);
$origin = UsageOrigin::createRegular($node, $scope);

$usage = $constantRef instanceof EnumCaseRef
? new EnumCaseUsage($origin, $constantRef)
: new ClassConstantUsage($origin, $constantRef);

$this->registerUsage($usage, $node, $scope);
}
}
}
Expand All @@ -178,7 +180,7 @@ private function getConstantNames(ClassConstFetch $fetch, Scope $scope): array
}

/**
* @return list<ClassConstantRef>
* @return list<ClassConstantRef|EnumCaseRef>
*/
private function getDeclaringTypesWithConstant(
Type $type,
Expand All @@ -190,20 +192,30 @@ private function getDeclaringTypesWithConstant(
$classReflections = $typeNormalized->getObjectTypeOrClassStringObjectType()->getObjectClassReflections();

$result = [];
$mayBeEnum = !$typeNormalized->isEnum()->no();

foreach ($classReflections as $classReflection) {
$possibleDescendant = $isPossibleDescendant ?? !$classReflection->isFinal();

if ($mayBeEnum) {
$result[] = new EnumCaseRef($classReflection->getName(), $constantName, $typeNormalized->isEnum()->maybe());
}

$result[] = new ClassConstantRef($classReflection->getName(), $constantName, $possibleDescendant);
}

if ($result === []) {
$result[] = new ClassConstantRef(null, $constantName, true); // call over unknown type
if ($result === []) { // call over unknown type
$result[] = new ClassConstantRef(null, $constantName, true);
$result[] = new EnumCaseRef(null, $constantName, true);
}

return $result;
}

private function registerUsage(ClassConstantUsage $usage, Node $node, Scope $scope): void
/**
* @param ClassConstantUsage|EnumCaseUsage $usage
*/
private function registerUsage(ClassMemberUsage $usage, Node $node, Scope $scope): void
{
$excluderName = null;

Expand Down
4 changes: 4 additions & 0 deletions src/Debug/DebugUsagePrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ private function printMixedEverythingUsages(Output $output, array $fullyMixedUsa
}

foreach ($fullyMixedUsages as $memberType => $collectedUsages) {
if ($memberType === MemberType::ENUM_CASE) {
continue; // any fully mixed fetch is emitted twice (once as enum, once as const); report only once here
}

$fullyMixedCount = count($collectedUsages);

$memberTypeString = $memberType === MemberType::METHOD ? 'method' : 'constant';
Expand Down
1 change: 1 addition & 0 deletions src/Enum/MemberType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ interface MemberType

public const METHOD = 1;
public const CONSTANT = 2;
public const ENUM_CASE = 3;

}
14 changes: 11 additions & 3 deletions src/Error/BlackMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use ShipMonk\PHPStan\DeadCode\Graph\EnumCaseRef;
use ShipMonk\PHPStan\DeadCode\Rule\DeadCodeRule;
use function array_keys;
use function count;
Expand Down Expand Up @@ -77,9 +79,15 @@ public function addExcludedUsage(CollectedUsage $excludedUsage): void

public function getErrorIdentifier(): string
{
return $this->member instanceof ClassConstantRef
? DeadCodeRule::IDENTIFIER_CONSTANT
: DeadCodeRule::IDENTIFIER_METHOD;
if ($this->member instanceof ClassConstantRef) {
return DeadCodeRule::IDENTIFIER_CONSTANT;
} elseif ($this->member instanceof ClassMethodRef) {
return DeadCodeRule::IDENTIFIER_METHOD;
} elseif ($this->member instanceof EnumCaseRef) {
return DeadCodeRule::IDENTIFIER_ENUM_CASE;
} else {
throw new LogicException('Unknown member type');
}
}

public function getExclusionMessage(): string
Expand Down
12 changes: 10 additions & 2 deletions src/Formatter/RemoveDeadCodeFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function formatErrors(
if (
$fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_METHOD
&& $fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_CONSTANT
&& $fileSpecificError->getIdentifier() !== DeadCodeRule::IDENTIFIER_ENUM_CASE
) {
continue;
}
Expand All @@ -77,10 +78,12 @@ public function formatErrors(
$deadConstants = $deadMembersByType[MemberType::CONSTANT] ?? [];
/** @var array<string, list<ClassMemberUsage>> $deadMethods */
$deadMethods = $deadMembersByType[MemberType::METHOD] ?? [];
/** @var array<string, list<ClassMemberUsage>> $deadEnumCases */
$deadEnumCases = $deadMembersByType[MemberType::ENUM_CASE] ?? [];

$membersCount += count($deadConstants) + count($deadMethods);
$membersCount += count($deadConstants) + count($deadMethods) + count($deadEnumCases);

$transformer = new RemoveDeadCodeTransformer(array_keys($deadMethods), array_keys($deadConstants));
$transformer = new RemoveDeadCodeTransformer(array_keys($deadMethods), array_keys($deadConstants), array_keys($deadEnumCases));
$oldCode = $this->fileSystem->read($file);
$newCode = $transformer->transformCode($oldCode);
$this->fileSystem->write($file, $newCode);
Expand All @@ -94,6 +97,11 @@ public function formatErrors(
$output->writeLineFormatted(" • Removed method <fg=white>$method</>");
$this->printExcludedUsages($output, $excludedUsages);
}

foreach ($deadEnumCases as $case => $excludedUsages) {
$output->writeLineFormatted(" • Removed enum case <fg=white>$case</>");
$this->printExcludedUsages($output, $excludedUsages);
}
}

$memberPlural = $membersCount === 1 ? '' : 's';
Expand Down
4 changes: 2 additions & 2 deletions src/Graph/ClassConstantRef.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ final class ClassConstantRef extends ClassMemberRef

public function __construct(
?string $className,
?string $constantName,
?string $enumCaseName,
bool $possibleDescendant
)
{
parent::__construct($className, $constantName, $possibleDescendant);
parent::__construct($className, $enumCaseName, $possibleDescendant);
}

public static function buildKey(string $typeName, string $memberName): string
Expand Down
17 changes: 13 additions & 4 deletions src/Graph/CollectedUsage.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,24 @@ public static function deserialize(string $data, string $scopeFile): self
);
$exclusionReason = $result['e'];

$usage = $memberType === MemberType::CONSTANT
? new ClassConstantUsage(
if ($memberType === MemberType::CONSTANT) {
$usage = new ClassConstantUsage(
$origin,
new ClassConstantRef($result['m']['c'], $result['m']['m'], $result['m']['d']),
)
: new ClassMethodUsage(
);
} elseif ($memberType === MemberType::METHOD) {
$usage = new ClassMethodUsage(
$origin,
new ClassMethodRef($result['m']['c'], $result['m']['m'], $result['m']['d']),
);
} elseif ($memberType === MemberType::ENUM_CASE) {
$usage = new EnumCaseUsage(
$origin,
new EnumCaseRef($result['m']['c'], $result['m']['m'], $result['m']['d']),
);
} else {
throw new LogicException('Unknown member type: ' . $memberType);
}

return new self($usage, $exclusionReason);
}
Expand Down
Loading
Loading