Skip to content

Commit 3a32065

Browse files
authored
Support stream_wrapper_register (#266)
1 parent 7cbc2b8 commit 3a32065

File tree

5 files changed

+179
-0
lines changed

5 files changed

+179
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ parameters:
115115
#### Enum:
116116
- Detects usages caused by `BackedEnum::from`, `BackedEnum::tryFrom` and `UnitEnum::cases`
117117

118+
#### StreamWrapper:
119+
- Detects usages caused by `stream_wrapper_register`
120+
118121
Those providers are enabled by default, but you can disable them if needed.
119122

120123
## Excluding usages in tests:

rules.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ services:
9999
arguments:
100100
enabled: %shipmonkDeadCode.usageProviders.nette.enabled%
101101

102+
-
103+
class: ShipMonk\PHPStan\DeadCode\Provider\StreamWrapperUsageProvider
104+
tags:
105+
- shipmonk.deadCode.memberUsageProvider
106+
arguments:
107+
enabled: %shipmonkDeadCode.usageProviders.streamWrapper.enabled%
108+
102109

103110
-
104111
class: ShipMonk\PHPStan\DeadCode\Excluder\TestsUsageExcluder
@@ -199,6 +206,8 @@ parameters:
199206
enabled: null
200207
nette:
201208
enabled: null
209+
streamWrapper:
210+
enabled: true
202211
usageExcluders:
203212
tests:
204213
enabled: false
@@ -255,6 +264,9 @@ parametersSchema:
255264
nette: structure([
256265
enabled: schema(bool(), nullable())
257266
])
267+
streamWrapper: structure([
268+
enabled: bool()
269+
])
258270
])
259271
usageExcluders: structure([
260272
tests: structure([
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\DeadCode\Provider;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\Scope;
9+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
10+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
11+
use ShipMonk\PHPStan\DeadCode\Graph\UsageOrigin;
12+
use function in_array;
13+
14+
/**
15+
* See: https://php.net/manual/en/class.streamwrapper.php
16+
*/
17+
class StreamWrapperUsageProvider implements MemberUsageProvider
18+
{
19+
20+
private const STREAM_WRAPPER_METHODS = [
21+
'dir_closedir',
22+
'dir_opendir',
23+
'dir_readdir',
24+
'dir_rewinddir',
25+
'mkdir',
26+
'rename',
27+
'rmdir',
28+
'stream_cast',
29+
'stream_close',
30+
'stream_eof',
31+
'stream_flush',
32+
'stream_lock',
33+
'stream_metadata',
34+
'stream_open',
35+
'stream_read',
36+
'stream_seek',
37+
'stream_set_option',
38+
'stream_stat',
39+
'stream_tell',
40+
'stream_truncate',
41+
'stream_write',
42+
'unlink',
43+
'url_stat',
44+
];
45+
46+
private bool $enabled;
47+
48+
public function __construct(
49+
bool $enabled
50+
)
51+
{
52+
$this->enabled = $enabled;
53+
}
54+
55+
public function getUsages(
56+
Node $node,
57+
Scope $scope
58+
): array
59+
{
60+
if (!$this->enabled) {
61+
return [];
62+
}
63+
64+
if (!$node instanceof FuncCall) {
65+
return [];
66+
}
67+
68+
return $this->processFunctionCall($node, $scope);
69+
}
70+
71+
/**
72+
* @return list<ClassMethodUsage>
73+
*/
74+
private function processFunctionCall(
75+
FuncCall $node,
76+
Scope $scope
77+
): array
78+
{
79+
$functionNames = $this->getFunctionNames($node, $scope);
80+
81+
if (in_array('stream_wrapper_register', $functionNames, true)) {
82+
return $this->handleStreamWrapperRegister($node, $scope);
83+
}
84+
85+
return [];
86+
}
87+
88+
/**
89+
* @return list<string>
90+
*/
91+
private function getFunctionNames(
92+
FuncCall $node,
93+
Scope $scope
94+
): array
95+
{
96+
if ($node->name instanceof Name) {
97+
return [$node->name->toString()];
98+
}
99+
100+
$functionNames = [];
101+
foreach ($scope->getType($node->name)->getConstantStrings() as $constantString) {
102+
$functionNames[] = $constantString->getValue();
103+
}
104+
105+
return $functionNames;
106+
}
107+
108+
/**
109+
* @return list<ClassMethodUsage>
110+
*/
111+
private function handleStreamWrapperRegister(
112+
FuncCall $node,
113+
Scope $scope
114+
): array
115+
{
116+
$secondArg = $node->getArgs()[1] ?? null;
117+
if ($secondArg === null) {
118+
return [];
119+
}
120+
121+
$argType = $scope->getType($secondArg->value);
122+
123+
$usages = [];
124+
$classNames = [];
125+
foreach ($argType->getConstantStrings() as $constantString) {
126+
$classNames[] = $constantString->getValue();
127+
}
128+
129+
foreach ($classNames as $className) {
130+
foreach (self::STREAM_WRAPPER_METHODS as $methodName) {
131+
$usages[] = new ClassMethodUsage(
132+
UsageOrigin::createRegular($node, $scope),
133+
new ClassMethodRef(
134+
$className,
135+
$methodName,
136+
false,
137+
),
138+
);
139+
}
140+
}
141+
142+
return $usages;
143+
}
144+
145+
}

tests/Rule/DeadCodeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitUsageProvider;
4848
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
4949
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionUsageProvider;
50+
use ShipMonk\PHPStan\DeadCode\Provider\StreamWrapperUsageProvider;
5051
use ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider;
5152
use ShipMonk\PHPStan\DeadCode\Provider\TwigUsageProvider;
5253
use ShipMonk\PHPStan\DeadCode\Provider\VendorUsageProvider;
@@ -863,6 +864,7 @@ public static function provideFiles(): Traversable
863864
yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php'];
864865
yield 'provider-apiphpdoc' => [__DIR__ . '/data/providers/api-phpdoc.php', self::requiresPhp(8_01_00)];
865866
yield 'provider-enum' => [__DIR__ . '/data/providers/enum.php', self::requiresPhp(8_01_00)];
867+
yield 'provider-stream-wrapper' => [__DIR__ . '/data/providers/stream-wrapper.php'];
866868

867869
// excluders
868870
yield 'excluder-tests' => [[__DIR__ . '/data/excluders/tests/src/code.php', __DIR__ . '/data/excluders/tests/tests/code.php']];
@@ -1009,6 +1011,9 @@ private function getMemberUsageProviders(): array
10091011
self::getContainer()->getByType(ReflectionProvider::class),
10101012
$this->providersEnabled,
10111013
),
1014+
new StreamWrapperUsageProvider(
1015+
$this->providersEnabled,
1016+
),
10121017
new SymfonyUsageProvider(
10131018
$this->createContainerMockWithSymfonyConfig(),
10141019
$this->providersEnabled,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace StreamWrapper;
4+
5+
class MyStreamWrapper
6+
{
7+
public function stream_open($path, $mode, $options, &$opened_path) {}
8+
public function stream_read($count) {}
9+
public function unrelated($path, $mode, $options, &$opened_path) {} // error: Unused StreamWrapper\MyStreamWrapper::unrelated
10+
}
11+
12+
function testAll() {
13+
stream_wrapper_register('myprotocol', MyStreamWrapper::class);
14+
}

0 commit comments

Comments
 (0)