Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cee4335
Detect dead enum cases
janedbal Apr 4, 2025
f62828e
Fix identifier; fix tests for old PHP
janedbal Apr 4, 2025
a4dc18c
Detect #[Column(enumType)], adjust ReflectionBasedMemberUsageProvider
janedbal Apr 7, 2025
d629d78
EnumUsageProvider
janedbal Apr 8, 2025
ebd2ffd
Reflection::getCases
janedbal Apr 10, 2025
dd4f7f0
Resolve enumCase fetch over interface
janedbal Apr 17, 2025
b029266
Rebase fixes
janedbal Jul 3, 2025
48b68c4
Fix composer-dependency-analyser for old PHP versions
janedbal Jul 4, 2025
74aceb4
Enum case is actually a constant internally in PHP, lets drop EnumCas…
janedbal Jul 14, 2025
aab2f55
New test: Ensure doctrine enumType wont mark constants as used
janedbal Jul 14, 2025
046a1c4
Minor method rename
janedbal Jul 14, 2025
472ea13
ConstantFetchCollector: emit isEnumCase=false when sure
janedbal Jul 14, 2025
be65c23
Test: ensure constant reflection methods mark both enum cases and con…
janedbal Jul 14, 2025
50dda4e
Fix debugging of enum cases
janedbal Jul 14, 2025
91ba580
Better test for custom reflection-based providers
janedbal Jul 15, 2025
0a25e52
ReflectionBasedMemberUsageProvider: dont pass enum cases to shouldMar…
janedbal Jul 15, 2025
0ecd2c2
polishing: keys methods + key building
janedbal Jul 15, 2025
2f3bd0b
Enum cases are not detected by default
janedbal Jul 16, 2025
d0ff794
Skip enum tests on PHP 8-
janedbal Jul 17, 2025
ee8cd1e
CI: do not run PHPStan on PHP 7.4, too many enum-related issues
janedbal Jul 17, 2025
8b4fb98
PHPStan: ignore ReflectionEnum not yet generic on PHP 8.0
janedbal Jul 17, 2025
5e64f72
Readme: mention support of Doctrine's enumType
janedbal Jul 18, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php-version: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ]
php-version: [ '8.0', '8.1', '8.2', '8.3', '8.4' ]
steps:
-
name: Checkout code
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ $ vendor/bin/phpstan
- `Doctrine\Common\EventSubscriber` methods
- `repositoryMethod` in `#[UniqueEntity]` attribute
- lifecycle event attributes `#[PreFlush]`, `#[PostLoad]`, ...
- enums in `#[Column(enumType: UserStatus::class)]`

#### PHPUnit:
- **data provider methods**
Expand Down Expand Up @@ -90,13 +91,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 Expand Up @@ -340,6 +344,21 @@ $methods = $reflection->getMethods(); // all Foo methods are used here

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

## Detected class members:
Default configuration is:

```neon
parameters:
shipmonkDeadCode:
detect:
deadMethods: true
deadConstants: true
deadEnumCases: false
```

Enum cases are disabled by default as those are often used in API input objects (using custom deserialization, which typically require custom usage provider).


## Comparison with tomasvotruba/unused-public
- You can see [detailed comparison PR](https://github.com/shipmonk-rnd/dead-code-detector/pull/53)
- Basically, their analysis is less precise and less flexible. Mainly:
Expand Down
12 changes: 11 additions & 1 deletion composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@

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

return (new Configuration())
$config = (new Configuration())
->ignoreErrorsOnPath(__DIR__ . '/src/Provider', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // providers are designed that way
->ignoreErrorsOnExtensionAndPath('ext-simplexml', __DIR__ . '/src/Provider/SymfonyUsageProvider.php', [ErrorType::SHADOW_DEPENDENCY]) // guarded with extension_loaded()
->addPathToExclude(__DIR__ . '/tests/Rule/data');

if (PHP_VERSION_ID < 80100) {
$config->ignoreUnknownClasses([
'ReflectionEnum',
'ReflectionEnumBackedCase',
'ReflectionEnumUnitCase'
]);
}

return $config;
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
2 changes: 1 addition & 1 deletion composer.lock

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

4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ parameters:
message: "#but it's missing from the PHPDoc @throws tag\\.$#" # allow uncatched exceptions in tests
path: tests/*

-
message: "#class ReflectionEnum is not generic#"
reportUnmatched: false # reported only with PHP 8.0

# allow referencing any attribute classes
- '#^Parameter \#1 \$name of method PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionClass\:\:getAttributes\(\) expects class\-string\|null, string given\.$#'
- '#^Parameter \#1 \$name of method PHPStan\\BetterReflection\\Reflection\\Adapter\\ReflectionMethod\:\:getAttributes\(\) expects class\-string\|null, string given\.$#'
25 changes: 25 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ services:
arguments:
enabled: %shipmonkDeadCode.usageProviders.apiPhpDoc.enabled%

-
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 @@ -113,6 +120,10 @@ services:
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
tags:
- phpstan.collector
arguments:
detectDeadMethods: %shipmonkDeadCode.detect.deadMethods%
detectDeadConstants: %shipmonkDeadCode.detect.deadConstants%
detectDeadEnumCases: %shipmonkDeadCode.detect.deadEnumCases%

-
class: ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector
Expand Down Expand Up @@ -144,9 +155,15 @@ parameters:
shipmonkDeadCode:
trackMixedAccess: null
reportTransitivelyDeadMethodAsSeparateError: false
detect:
deadMethods: true
deadConstants: true
deadEnumCases: false
usageProviders:
apiPhpDoc:
enabled: true
enum:
enabled: true
vendor:
enabled: true
reflection:
Expand Down Expand Up @@ -177,10 +194,18 @@ parametersSchema:
shipmonkDeadCode: structure([
trackMixedAccess: schema(bool(), nullable()) # deprecated, use usageExcluders.usageOverMixed.enabled
reportTransitivelyDeadMethodAsSeparateError: bool()
detect: structure([
deadMethods: bool()
deadConstants: bool()
deadEnumCases: bool()
])
usageProviders: structure([
apiPhpDoc: structure([
enabled: bool()
])
enum: structure([
enabled: bool()
])
vendor: structure([
enabled: bool()
])
Expand Down
78 changes: 64 additions & 14 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 All @@ -37,9 +39,23 @@ class ClassDefinitionCollector implements Collector

private ReflectionProvider $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
private bool $detectDeadMethods;

private bool $detectDeadConstants;

private bool $detectDeadEnumCases;

public function __construct(
ReflectionProvider $reflectionProvider,
bool $detectDeadMethods,
bool $detectDeadConstants,
bool $detectDeadEnumCases
)
{
$this->reflectionProvider = $reflectionProvider;
$this->detectDeadMethods = $detectDeadMethods;
$this->detectDeadConstants = $detectDeadConstants;
$this->detectDeadEnumCases = $detectDeadEnumCases;
}

public function getNodeType(): string
Expand All @@ -52,6 +68,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,22 +90,34 @@ public function processNode(
$reflection = $this->reflectionProvider->getClass($typeName);

$methods = [];

foreach ($node->getMethods() as $method) {
$methods[$method->name->toString()] = [
'line' => $method->name->getStartLine(),
'params' => count($method->params),
'abstract' => $method->isAbstract() || $node instanceof Interface_,
'visibility' => $method->flags & (Visibility::PUBLIC | Visibility::PROTECTED | Visibility::PRIVATE),
];
$constants = [];
$cases = [];

if ($this->detectDeadMethods) {
foreach ($node->getMethods() as $method) {
$methods[$method->name->toString()] = [
'line' => $method->name->getStartLine(),
'params' => count($method->params),
'abstract' => $method->isAbstract() || $node instanceof Interface_,
'visibility' => $method->flags & (Visibility::PUBLIC | Visibility::PROTECTED | Visibility::PRIVATE),
];
}
}

$constants = [];
if ($this->detectDeadConstants) {
foreach ($node->getConstants() as $constant) {
foreach ($constant->consts as $const) {
$constants[$const->name->toString()] = [
'line' => $const->getStartLine(),
];
}
}
}

foreach ($node->getConstants() as $constant) {
foreach ($constant->consts as $const) {
$constants[$const->name->toString()] = [
'line' => $const->getStartLine(),
if ($this->detectDeadEnumCases) {
foreach ($this->getEnumCases($node) as $case) {
$cases[$case->name->toString()] = [
'line' => $case->name->getStartLine(),
];
}
}
Expand All @@ -97,6 +126,7 @@ public function processNode(
'kind' => $kind,
'name' => $typeName,
'methods' => $methods,
'cases' => $cases,
'constants' => $constants,
'parents' => $this->getParents($reflection),
'traits' => $this->getTraits($node),
Expand Down Expand Up @@ -182,4 +212,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;
}

}
34 changes: 19 additions & 15 deletions src/Collector/ConstantFetchCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeUtils;
Expand Down Expand Up @@ -121,7 +122,7 @@ private function registerFunctionCall(
$this->registerUsage(
new ClassConstantUsage(
UsageOrigin::createRegular($node, $scope),
new ClassConstantRef($className, $constantName, true),
new ClassConstantRef($className, $constantName, true, TrinaryLogic::createMaybe()),
),
$node,
$scope,
Expand Down Expand Up @@ -151,14 +152,10 @@ private function registerFetch(
}

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 = new ClassConstantUsage($origin, $constantRef);

$this->registerUsage($usage, $node, $scope);
}
}
}
Expand Down Expand Up @@ -187,26 +184,33 @@ private function getConstantNames(
}

/**
* @return list<ClassConstantRef>
* @return list<ClassConstantRef<?string, ?string>>
*/
private function getDeclaringTypesWithConstant(
Type $type,
?string $constantName,
?bool $isPossibleDescendant
): array
{
$typeNormalized = TypeUtils::toBenevolentUnion($type); // extract possible fetches even from Class|int
$classReflections = $typeNormalized->getObjectTypeOrClassStringObjectType()->getObjectClassReflections();
$typeNormalized = TypeUtils::toBenevolentUnion($type) // extract possible fetches even from Class|int
->getObjectTypeOrClassStringObjectType();
$classReflections = $typeNormalized->getObjectClassReflections();

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

foreach ($classReflections as $classReflection) {
$possibleDescendant = $isPossibleDescendant ?? !$classReflection->isFinal();
$result[] = new ClassConstantRef($classReflection->getName(), $constantName, $possibleDescendant);
$result[] = new ClassConstantRef(
$classReflection->getName(),
$constantName,
$possibleDescendant,
$isEnumCaseFetch,
);
}

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

return $result;
Expand Down
2 changes: 1 addition & 1 deletion src/Collector/MethodCallCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ private function getMethodNames(
}

/**
* @return list<ClassMethodRef>
* @return list<ClassMethodRef<?string, ?string>>
*/
private function getDeclaringTypesWithMethod(
?string $methodName,
Expand Down
Loading