Skip to content

Commit e3ef123

Browse files
authored
MemberUsageExcluder: support exclude of test usages for src classes (#139)
1 parent c976e55 commit e3ef123

23 files changed

+839
-190
lines changed

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
-**PHPStan** extension
88
- ♻️ **Dead cycles** detection
99
- 🔗 **Transitive dead** member detection
10+
- 🧪 **Dead tested code** detection
1011
- 🧹 **Automatic removal** of unused code
1112
- 📚 **Popular libraries** support
1213
-**Customizable** usage providers
@@ -65,7 +66,6 @@ All those libraries are autoenabled when found within your composer dependencies
6566
If you want to force enable/disable some of them, you can:
6667

6768
```neon
68-
# phpstan.neon.dist
6969
parameters:
7070
shipmonkDeadCode:
7171
usageProviders:
@@ -85,12 +85,26 @@ parameters:
8585

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

88+
## Excluding usages in tests:
89+
- By default, all usages within scanned paths can mark members as used
90+
- But that might not be desirable if class declared in `src` is **only used in `tests`**
91+
- You can exclude those usages by enabling `tests` usage excluder:
92+
93+
```neon
94+
parameters:
95+
shipmonkDeadCode:
96+
usageExcluders:
97+
tests:
98+
enabled: true
99+
devPaths: # optional, autodetects from autoload-dev sections of composer.json when omitted
100+
- %currentWorkingDirectory%/tests
101+
```
102+
88103
## Customization:
89104
- If your application does some magic calls unknown to this library, you can implement your own usage provider.
90105
- Just tag it with `shipmonk.deadCode.memberUsageProvider` and implement `ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider`
91106

92107
```neon
93-
# phpstan.neon.dist
94108
services:
95109
-
96110
class: App\ApiOutputUsageProvider
@@ -176,6 +190,36 @@ class DeserializationUsageProvider implements MemberUsageProvider
176190
}
177191
```
178192

193+
### Excluding usages:
194+
195+
You can exclude any usage based on custom logic, just implement `MemberUsageExcluder` and register it with `shipmonk.deadCode.memberUsageExcluder` tag:
196+
197+
```php
198+
199+
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
200+
201+
class MyUsageExcluder implements MemberUsageExcluder
202+
{
203+
204+
public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool
205+
{
206+
// ...
207+
}
208+
209+
}
210+
```
211+
212+
```neon
213+
# phpstan.neon.dist
214+
services:
215+
-
216+
class: App\MyUsageExcluder
217+
tags:
218+
- shipmonk.deadCode.memberUsageExcluder
219+
```
220+
221+
The same interface is used for exclusion of test-only usages, see above.
222+
179223
## Dead cycles & transitively dead methods
180224
- This library automatically detects dead cycles and transitively dead methods (methods that are only called from dead methods)
181225
- By default, it reports only the first dead method in the subtree and the rest as a tip:
@@ -221,6 +265,8 @@ class UserFacade
221265
}
222266
```
223267

268+
- If you are excluding tests usages (see above), this will not cause the related tests to be removed alongside.
269+
224270

225271
## Calls over unknown types
226272
- In order to prevent false positives, we support even calls over unknown types (e.g. `$unknown->method()`) by marking all methods named `method` as used

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "shipmonk/dead-code-detector",
3-
"description": "Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles.",
3+
"description": "Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.",
44
"license": [
55
"MIT"
66
],

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ parameters:
4444
ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider: UsageProvider
4545
enforceReadonlyPublicProperty:
4646
enabled: false # we support even PHP 7.4
47+
enforceClosureParamNativeTypehint:
48+
enabled: false # we support even PHP 7.4 (cannot use mixed nor unions)
4749

4850
ignoreErrors:
4951
-

rules.neon

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,31 @@ services:
5959
arguments:
6060
enabled: %shipmonkDeadCode.usageProviders.nette.enabled%
6161

62+
63+
-
64+
class: ShipMonk\PHPStan\DeadCode\Excluder\TestsUsageExcluder
65+
tags:
66+
- shipmonk.deadCode.memberUsageExcluder
67+
arguments:
68+
enabled: %shipmonkDeadCode.usageExcluders.tests.enabled%
69+
devPaths: %shipmonkDeadCode.usageExcluders.tests.devPaths%
70+
71+
6272
-
6373
class: ShipMonk\PHPStan\DeadCode\Collector\MethodCallCollector
6474
tags:
6575
- phpstan.collector
6676
arguments:
6777
trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
78+
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)
6879

6980
-
7081
class: ShipMonk\PHPStan\DeadCode\Collector\ConstantFetchCollector
7182
tags:
7283
- phpstan.collector
7384
arguments:
7485
trackMixedAccess: %shipmonkDeadCode.trackMixedAccess%
86+
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)
7587

7688
-
7789
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
@@ -84,6 +96,7 @@ services:
8496
- phpstan.collector
8597
arguments:
8698
memberUsageProviders: tagged(shipmonk.deadCode.memberUsageProvider)
99+
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)
87100

88101
-
89102
class: ShipMonk\PHPStan\DeadCode\Rule\DeadCodeRule
@@ -120,6 +133,10 @@ parameters:
120133
enabled: null
121134
nette:
122135
enabled: null
136+
usageExcluders:
137+
tests:
138+
enabled: false
139+
devPaths: null
123140

124141
parametersSchema:
125142
shipmonkDeadCode: structure([
@@ -149,4 +166,10 @@ parametersSchema:
149166
enabled: schema(bool(), nullable())
150167
])
151168
])
169+
usageExcluders: structure([
170+
tests: structure([
171+
enabled: bool()
172+
devPaths: schema(listOf(string()), nullable())
173+
])
174+
])
152175
])

src/Collector/BufferedUsageCollector.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\ClassMethodsNode;
8-
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
8+
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
99
use function array_map;
1010

1111
trait BufferedUsageCollector
1212
{
1313

1414
/**
15-
* @var list<ClassMemberUsage>
15+
* @var list<CollectedUsage>
1616
*/
1717
private array $usageBuffer = [];
1818

@@ -32,7 +32,7 @@ private function tryFlushBuffer(
3232
return $data === []
3333
? null
3434
: array_map(
35-
static fn (ClassMemberUsage $call): string => $call->serialize(),
35+
static fn (CollectedUsage $usage): string => $usage->serialize(),
3636
$data,
3737
);
3838
}

src/Collector/ConstantFetchCollector.php

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
use PHPStan\Type\Constant\ConstantStringType;
1515
use PHPStan\Type\Type;
1616
use PHPStan\Type\TypeUtils;
17+
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
1718
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
1819
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
20+
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
1921
use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
2022
use function array_map;
2123
use function count;
@@ -37,15 +39,25 @@ class ConstantFetchCollector implements Collector
3739

3840
private bool $trackMixedAccess;
3941

42+
/**
43+
* @var list<MemberUsageExcluder>
44+
*/
45+
private array $memberUsageExcluders;
46+
47+
/**
48+
* @param list<MemberUsageExcluder> $memberUsageExcluders
49+
*/
4050
public function __construct(
4151
UsageOriginDetector $usageOriginDetector,
4252
ReflectionProvider $reflectionProvider,
43-
bool $trackMixedAccess
53+
bool $trackMixedAccess,
54+
array $memberUsageExcluders
4455
)
4556
{
4657
$this->reflectionProvider = $reflectionProvider;
4758
$this->trackMixedAccess = $trackMixedAccess;
4859
$this->usageOriginDetector = $usageOriginDetector;
60+
$this->memberUsageExcluders = $memberUsageExcluders;
4961
}
5062

5163
public function getNodeType(): string
@@ -111,9 +123,13 @@ private function registerFunctionCall(FuncCall $node, Scope $scope): void
111123
}
112124
}
113125

114-
$this->usageBuffer[] = new ClassConstantUsage(
115-
$this->usageOriginDetector->detectOrigin($scope),
116-
new ClassConstantRef($className, $constantName, true),
126+
$this->registerUsage(
127+
new ClassConstantUsage(
128+
$this->usageOriginDetector->detectOrigin($scope),
129+
new ClassConstantRef($className, $constantName, true),
130+
),
131+
$node,
132+
$scope,
117133
);
118134
}
119135
}
@@ -139,9 +155,13 @@ private function registerFetch(ClassConstFetch $node, Scope $scope): void
139155
}
140156

141157
foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName) as $className) {
142-
$this->usageBuffer[] = new ClassConstantUsage(
143-
$this->usageOriginDetector->detectOrigin($scope),
144-
new ClassConstantRef($className, $constantName, $possibleDescendantFetch),
158+
$this->registerUsage(
159+
new ClassConstantUsage(
160+
$this->usageOriginDetector->detectOrigin($scope),
161+
new ClassConstantRef($className, $constantName, $possibleDescendantFetch),
162+
),
163+
$node,
164+
$scope,
145165
);
146166
}
147167
}
@@ -176,4 +196,18 @@ private function getDeclaringTypesWithConstant(
176196
return $result;
177197
}
178198

199+
private function registerUsage(ClassConstantUsage $usage, Node $node, Scope $scope): void
200+
{
201+
$excluderName = null;
202+
203+
foreach ($this->memberUsageExcluders as $excludedUsageDecider) {
204+
if ($excludedUsageDecider->shouldExclude($usage, $node, $scope)) {
205+
$excluderName = $excludedUsageDecider->getIdentifier();
206+
break;
207+
}
208+
}
209+
210+
$this->usageBuffer[] = new CollectedUsage($usage, $excluderName);
211+
}
212+
179213
}

0 commit comments

Comments
 (0)