Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ parameters:
#### Enum:
- Detects usages caused by `BackedEnum::from`, `BackedEnum::tryFrom` and `UnitEnum::cases`

#### StreamWrapper:
- Detects usages caused by `stream_wrapper_register`

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

## Excluding usages in tests:
Expand Down
12 changes: 12 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ services:
arguments:
enabled: %shipmonkDeadCode.usageProviders.nette.enabled%

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


-
class: ShipMonk\PHPStan\DeadCode\Excluder\TestsUsageExcluder
Expand Down Expand Up @@ -199,6 +206,8 @@ parameters:
enabled: null
nette:
enabled: null
streamWrapper:
enabled: true
usageExcluders:
tests:
enabled: false
Expand Down Expand Up @@ -255,6 +264,9 @@ parametersSchema:
nette: structure([
enabled: schema(bool(), nullable())
])
streamWrapper: structure([
enabled: bool()
])
])
usageExcluders: structure([
tests: structure([
Expand Down
145 changes: 145 additions & 0 deletions src/Provider/StreamWrapperUsageProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\DeadCode\Provider;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
use function in_array;

/**
* See: https://php.net/manual/en/class.streamwrapper.php
*/
class StreamWrapperUsageProvider implements MemberUsageProvider
{

private const STREAM_WRAPPER_METHODS = [
'dir_closedir',
'dir_opendir',
'dir_readdir',
'dir_rewinddir',
'mkdir',
'rename',
'rmdir',
'stream_cast',
'stream_close',
'stream_eof',
'stream_flush',
'stream_lock',
'stream_metadata',
'stream_open',
'stream_read',
'stream_seek',
'stream_set_option',
'stream_stat',
'stream_tell',
'stream_truncate',
'stream_write',
'unlink',
'url_stat',
];

private bool $enabled;

public function __construct(
bool $enabled
)
{
$this->enabled = $enabled;
}

public function getUsages(
Node $node,
Scope $scope
): array
{
if (!$this->enabled) {
return [];
}

if (!$node instanceof FuncCall) {
return [];
}

return $this->processFunctionCall($node, $scope);
}

/**
* @return list<ClassMethodUsage>
*/
private function processFunctionCall(
FuncCall $node,
Scope $scope
): array
{
$functionNames = $this->getFunctionNames($node, $scope);

if (in_array('stream_wrapper_register', $functionNames, true)) {
return $this->handleStreamWrapperRegister($node, $scope);
}

return [];
}

/**
* @return list<string>
*/
private function getFunctionNames(
FuncCall $node,
Scope $scope
): array
{
if ($node->name instanceof Name) {
return [$node->name->toString()];
}

$functionNames = [];
foreach ($scope->getType($node->name)->getConstantStrings() as $constantString) {
$functionNames[] = $constantString->getValue();
}

return $functionNames;
}

/**
* @return list<ClassMethodUsage>
*/
private function handleStreamWrapperRegister(
FuncCall $node,
Scope $scope
): array
{
$secondArg = $node->getArgs()[1] ?? null;
if ($secondArg === null) {
return [];
}

$argType = $scope->getType($secondArg->value);

$usages = [];
$classNames = [];
foreach ($argType->getConstantStrings() as $constantString) {
$classNames[] = $constantString->getValue();
}

foreach ($classNames as $className) {
foreach (self::STREAM_WRAPPER_METHODS as $methodName) {
$usages[] = new ClassMethodUsage(
UsageOrigin::createRegular($node, $scope),
new ClassMethodRef(
$className,
$methodName,
false,
),
);
Comment on lines +131 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smart. I like it.

}
}

return $usages;
}

}
5 changes: 5 additions & 0 deletions tests/Rule/DeadCodeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\StreamWrapperUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\TwigUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider;
Expand Down Expand Up @@ -863,6 +864,7 @@ public static function provideFiles(): Traversable
yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php'];
yield 'provider-apiphpdoc' => [__DIR__ . '/data/providers/api-phpdoc.php', self::requiresPhp(8_01_00)];
yield 'provider-enum' => [__DIR__ . '/data/providers/enum.php', self::requiresPhp(8_01_00)];
yield 'provider-stream-wrapper' => [__DIR__ . '/data/providers/stream-wrapper.php'];

// excluders
yield 'excluder-tests' => [[__DIR__ . '/data/excluders/tests/src/code.php', __DIR__ . '/data/excluders/tests/tests/code.php']];
Expand Down Expand Up @@ -1009,6 +1011,9 @@ private function getMemberUsageProviders(): array
self::getContainer()->getByType(ReflectionProvider::class),
$this->providersEnabled,
),
new StreamWrapperUsageProvider(
$this->providersEnabled,
),
new SymfonyUsageProvider(
$this->createContainerMockWithSymfonyConfig(),
$this->providersEnabled,
Expand Down
14 changes: 14 additions & 0 deletions tests/Rule/data/providers/stream-wrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace StreamWrapper;

class MyStreamWrapper
{
public function stream_open($path, $mode, $options, &$opened_path) {}
public function stream_read($count) {}
public function unrelated($path, $mode, $options, &$opened_path) {} // error: Unused StreamWrapper\MyStreamWrapper::unrelated
}

function testAll() {
stream_wrapper_register('myprotocol', MyStreamWrapper::class);
}