Skip to content
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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ parameters:
## Generic usage providers:

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

#### Vendor:
Expand Down Expand Up @@ -376,10 +376,12 @@ parameters:
detect:
deadMethods: true
deadConstants: true
deadEnumCases: false
deadProperties: false # opt-in
deadEnumCases: false # opt-in
```

Enum cases are disabled by default as those are often used in API input objects (using custom deserialization, which typically require custom usage provider).
Enum cases and properties are disabled by default as those are often used in API input objects (using custom deserialization, which typically require custom usage provider).
But libraries should be able to enable those easily.


## Comparison with tomasvotruba/unused-public
Expand Down Expand Up @@ -477,8 +479,9 @@ If you set up `editorUrl` [parameter](https://phpstan.org/user-guide/output-form
- You can also mark whole class or interface with `@api` to mark all its methods as entrypoints

## Future scope:
- Dead class property detection
- Dead class detection
- Dead parameters detection
- Useless public/protected visibility

## Contributing
- Check your code by `composer check`
Expand Down
14 changes: 14 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,21 @@ services:
arguments:
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)

-
class: ShipMonk\PHPStan\DeadCode\Collector\PropertyAccessCollector
tags:
- phpstan.collector
arguments:
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)

-
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
tags:
- phpstan.collector
arguments:
detectDeadConstants: %shipmonkDeadCode.detect.deadConstants%
detectDeadEnumCases: %shipmonkDeadCode.detect.deadEnumCases%
detectDeadProperties: %shipmonkDeadCode.detect.deadProperties%

-
class: ShipMonk\PHPStan\DeadCode\Collector\ProvidedUsagesCollector
Expand All @@ -175,6 +183,10 @@ services:
servicesWithOldTag: tagged(shipmonk.deadCode.entrypointProvider)
trackMixedAccessParameterValue: %shipmonkDeadCode.trackMixedAccess%

-
class: ShipMonk\PHPStan\DeadCode\Visitor\PropertyWriteVisitor
tags:
- phpstan.parser.richParserNodeVisitor

parameters:
parametersNotInvalidatingCache:
Expand All @@ -187,6 +199,7 @@ parameters:
deadMethods: true
deadConstants: true
deadEnumCases: false
deadProperties: false
usageProviders:
apiPhpDoc:
enabled: true
Expand Down Expand Up @@ -238,6 +251,7 @@ parametersSchema:
deadMethods: bool()
deadConstants: bool()
deadEnumCases: bool()
deadProperties: bool()
])
usageProviders: structure([
apiPhpDoc: structure([
Expand Down
63 changes: 50 additions & 13 deletions src/Collector/ClassDefinitionCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use LogicException;
use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
Expand All @@ -21,13 +22,15 @@
use function array_fill_keys;
use function array_map;
use function count;
use function is_string;

/**
* @implements Collector<ClassLike, array{
* kind: string,
* name: string,
* cases: array<string, array{line: int}>,
* constants: array<string, array{line: int}>,
* properties: array<string, array{line: int}>,
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
Expand All @@ -43,15 +46,19 @@ final class ClassDefinitionCollector implements Collector

private bool $detectDeadEnumCases;

private bool $detectDeadProperties;

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

public function getNodeType(): string
Expand All @@ -66,6 +73,7 @@ public function getNodeType(): string
* name: string,
* cases: array<string, array{line: int}>,
* constants: array<string, array{line: int}>,
* properties: array<string, array{line: int}>,
* methods: array<string, array{line: int, params: int, abstract: bool, visibility: int-mask-of<Visibility::*>}>,
* parents: array<string, null>,
* traits: array<string, array{excluded?: list<string>, aliases?: array<string, string>}>,
Expand All @@ -88,40 +96,69 @@ public function processNode(
$methods = [];
$constants = [];
$cases = [];
$properties = [];

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

if ($this->detectDeadConstants) {
foreach ($node->getConstants() as $constant) {
foreach ($constant->consts as $const) {
$constants[$const->name->toString()] = [
'line' => $const->getStartLine(),
];
if ($methodName === '__construct') {
foreach ($method->getParams() as $param) {
if ($param->isPromoted() && $param->var instanceof Variable && is_string($param->var->name)) {
$properties[$param->var->name] = [
'line' => $param->getStartLine(),
];
}
}
}
}

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

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

foreach ($node->getProperties() as $property) {
foreach ($property->props as $prop) {
$properties[$prop->name->toString()] = [
'line' => $prop->getStartLine(),
];
}
}

if (!$this->detectDeadConstants) {
$constants = [];
}

if (!$this->detectDeadEnumCases) {
$cases = [];
}

if (!$this->detectDeadProperties) {
$properties = [];
}

return [
'kind' => $kind,
'name' => $typeName,
'methods' => $methods,
'cases' => $cases,
'constants' => $constants,
'properties' => $properties,
'parents' => $this->getParents($reflection),
'traits' => $this->getTraits($node),
'interfaces' => $this->getInterfaces($reflection),
Expand Down
Loading