Skip to content
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

MemberUsageExcluder: support exclude of test usages for src classes #139

Merged
merged 8 commits into from
Mar 3, 2025
Merged
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
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- ✅ **PHPStan** extension
- ♻️ **Dead cycles** detection
- 🔗 **Transitive dead** member detection
- 🧪 **Dead tested code** detection
- 🧹 **Automatic removal** of unused code
- 📚 **Popular libraries** support
- ✨ **Customizable** usage providers
Expand Down Expand Up @@ -65,7 +66,6 @@ All those libraries are autoenabled when found within your composer dependencies
If you want to force enable/disable some of them, you can:

```neon
# phpstan.neon.dist
parameters:
shipmonkDeadCode:
usageProviders:
Expand All @@ -85,12 +85,26 @@ parameters:

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

## Excluding usages in tests:
- By default, all usages within scanned paths can mark members as used
- But that might not be desirable if class declared in `src` is **only used in `tests`**
- You can exclude those usages by enabling `tests` usage excluder:

```neon
parameters:
shipmonkDeadCode:
usageExcluders:
tests:
enabled: true
devPaths: # optional, autodetects from autoload-dev sections of composer.json when omitted
- %currentWorkingDirectory%/tests
```

## Customization:
- If your application does some magic calls unknown to this library, you can implement your own usage provider.
- Just tag it with `shipmonk.deadCode.memberUsageProvider` and implement `ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider`

```neon
# phpstan.neon.dist
services:
-
class: App\ApiOutputUsageProvider
Expand Down Expand Up @@ -176,6 +190,36 @@ class DeserializationUsageProvider implements MemberUsageProvider
}
```

### Excluding usages:

You can exclude any usage based on custom logic, just implement `MemberUsageExcluder` and register it with `shipmonk.deadCode.memberUsageExcluder` tag:

```php

use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;

class MyUsageExcluder implements MemberUsageExcluder
{

public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool
{
// ...
}

}
```

```neon
# phpstan.neon.dist
services:
-
class: App\MyUsageExcluder
tags:
- shipmonk.deadCode.memberUsageExcluder
```

The same interface is used for exclusion of test-only usages, see above.

## Dead cycles & transitively dead methods
- This library automatically detects dead cycles and transitively dead methods (methods that are only called from dead methods)
- By default, it reports only the first dead method in the subtree and the rest as a tip:
Expand Down Expand Up @@ -221,6 +265,8 @@ class UserFacade
}
```

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


## Calls over unknown types
- 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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shipmonk/dead-code-detector",
"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.",
"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.",
"license": [
"MIT"
],
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ parameters:
ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider: UsageProvider
enforceReadonlyPublicProperty:
enabled: false # we support even PHP 7.4
enforceClosureParamNativeTypehint:
enabled: false # we support even PHP 7.4 (cannot use mixed nor unions)

ignoreErrors:
-
Expand Down
23 changes: 23 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,31 @@ services:
arguments:
enabled: %shipmonkDeadCode.usageProviders.nette.enabled%


-
class: ShipMonk\PHPStan\DeadCode\Excluder\TestsUsageExcluder
tags:
- shipmonk.deadCode.memberUsageExcluder
arguments:
enabled: %shipmonkDeadCode.usageExcluders.tests.enabled%
devPaths: %shipmonkDeadCode.usageExcluders.tests.devPaths%


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

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

-
class: ShipMonk\PHPStan\DeadCode\Collector\ClassDefinitionCollector
Expand All @@ -84,6 +96,7 @@ services:
- phpstan.collector
arguments:
memberUsageProviders: tagged(shipmonk.deadCode.memberUsageProvider)
memberUsageExcluders: tagged(shipmonk.deadCode.memberUsageExcluder)

-
class: ShipMonk\PHPStan\DeadCode\Rule\DeadCodeRule
Expand Down Expand Up @@ -120,6 +133,10 @@ parameters:
enabled: null
nette:
enabled: null
usageExcluders:
tests:
enabled: false
devPaths: null

parametersSchema:
shipmonkDeadCode: structure([
Expand Down Expand Up @@ -149,4 +166,10 @@ parametersSchema:
enabled: schema(bool(), nullable())
])
])
usageExcluders: structure([
tests: structure([
enabled: bool()
devPaths: schema(listOf(string()), nullable())
])
])
])
6 changes: 3 additions & 3 deletions src/Collector/BufferedUsageCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\ClassMethodsNode;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMemberUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use function array_map;

trait BufferedUsageCollector
{

/**
* @var list<ClassMemberUsage>
* @var list<CollectedUsage>
*/
private array $usageBuffer = [];

Expand All @@ -32,7 +32,7 @@ private function tryFlushBuffer(
return $data === []
? null
: array_map(
static fn (ClassMemberUsage $call): string => $call->serialize(),
static fn (CollectedUsage $usage): string => $usage->serialize(),
$data,
);
}
Expand Down
48 changes: 41 additions & 7 deletions src/Collector/ConstantFetchCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeUtils;
use ShipMonk\PHPStan\DeadCode\Excluder\MemberUsageExcluder;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassConstantUsage;
use ShipMonk\PHPStan\DeadCode\Graph\CollectedUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOriginDetector;
use function array_map;
use function count;
Expand All @@ -37,15 +39,25 @@ class ConstantFetchCollector implements Collector

private bool $trackMixedAccess;

/**
* @var list<MemberUsageExcluder>
*/
private array $memberUsageExcluders;

/**
* @param list<MemberUsageExcluder> $memberUsageExcluders
*/
public function __construct(
UsageOriginDetector $usageOriginDetector,
ReflectionProvider $reflectionProvider,
bool $trackMixedAccess
bool $trackMixedAccess,
array $memberUsageExcluders
)
{
$this->reflectionProvider = $reflectionProvider;
$this->trackMixedAccess = $trackMixedAccess;
$this->usageOriginDetector = $usageOriginDetector;
$this->memberUsageExcluders = $memberUsageExcluders;
}

public function getNodeType(): string
Expand Down Expand Up @@ -111,9 +123,13 @@ private function registerFunctionCall(FuncCall $node, Scope $scope): void
}
}

$this->usageBuffer[] = new ClassConstantUsage(
$this->usageOriginDetector->detectOrigin($scope),
new ClassConstantRef($className, $constantName, true),
$this->registerUsage(
new ClassConstantUsage(
$this->usageOriginDetector->detectOrigin($scope),
new ClassConstantRef($className, $constantName, true),
),
$node,
$scope,
);
}
}
Expand All @@ -139,9 +155,13 @@ private function registerFetch(ClassConstFetch $node, Scope $scope): void
}

foreach ($this->getDeclaringTypesWithConstant($ownerType, $constantName) as $className) {
$this->usageBuffer[] = new ClassConstantUsage(
$this->usageOriginDetector->detectOrigin($scope),
new ClassConstantRef($className, $constantName, $possibleDescendantFetch),
$this->registerUsage(
new ClassConstantUsage(
$this->usageOriginDetector->detectOrigin($scope),
new ClassConstantRef($className, $constantName, $possibleDescendantFetch),
),
$node,
$scope,
);
}
}
Expand Down Expand Up @@ -176,4 +196,18 @@ private function getDeclaringTypesWithConstant(
return $result;
}

private function registerUsage(ClassConstantUsage $usage, Node $node, Scope $scope): void
{
$excluderName = null;

foreach ($this->memberUsageExcluders as $excludedUsageDecider) {
if ($excludedUsageDecider->shouldExclude($usage, $node, $scope)) {
$excluderName = $excludedUsageDecider->getIdentifier();
break;
}
}

$this->usageBuffer[] = new CollectedUsage($usage, $excluderName);
}

}
Loading