Skip to content

Commit c533a6e

Browse files
authored
Treat #[Pure(true)] in PhpStorm stubs as hasSideEffects => true
1 parent 2fe4e0f commit c533a6e

File tree

3 files changed

+207
-139
lines changed

3 files changed

+207
-139
lines changed

Diff for: bin/functionMetadata_original.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,13 @@
7070
'chown' => ['hasSideEffects' => true],
7171
'copy' => ['hasSideEffects' => true],
7272
'count' => ['hasSideEffects' => false],
73-
'connection_aborted' => ['hasSideEffects' => true],
74-
'connection_status' => ['hasSideEffects' => true],
7573
'error_log' => ['hasSideEffects' => true],
7674
'fclose' => ['hasSideEffects' => true],
7775
'fflush' => ['hasSideEffects' => true],
7876
'fgetc' => ['hasSideEffects' => true],
7977
'fgetcsv' => ['hasSideEffects' => true],
8078
'fgets' => ['hasSideEffects' => true],
8179
'fgetss' => ['hasSideEffects' => true],
82-
'file_get_contents' => ['hasSideEffects' => true],
8380
'file_put_contents' => ['hasSideEffects' => true],
8481
'flock' => ['hasSideEffects' => true],
8582
'fopen' => ['hasSideEffects' => true],
@@ -98,6 +95,18 @@
9895
'mb_str_pad' => ['hasSideEffects' => false],
9996
'mkdir' => ['hasSideEffects' => true],
10097
'move_uploaded_file' => ['hasSideEffects' => true],
98+
'ob_clean' => ['hasSideEffects' => true],
99+
'ob_end_clean' => ['hasSideEffects' => true],
100+
'ob_end_flush' => ['hasSideEffects' => true],
101+
'ob_flush' => ['hasSideEffects' => true],
102+
'ob_get_clean' => ['hasSideEffects' => true],
103+
'ob_get_contents' => ['hasSideEffects' => true],
104+
'ob_get_length' => ['hasSideEffects' => true],
105+
'ob_get_level' => ['hasSideEffects' => true],
106+
'ob_get_status' => ['hasSideEffects' => true],
107+
'ob_list_handlers' => ['hasSideEffects' => true],
108+
'output_add_rewrite_var' => ['hasSideEffects' => true],
109+
'output_reset_rewrite_vars' => ['hasSideEffects' => true],
101110
'pclose' => ['hasSideEffects' => true],
102111
'popen' => ['hasSideEffects' => true],
103112
'readfile' => ['hasSideEffects' => true],

Diff for: bin/generate-function-metadata.php

+61-13
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,63 @@
2525
/** @var string[] */
2626
public array $functions = [];
2727

28+
/** @var list<string> */
29+
public array $impureFunctions = [];
30+
2831
/** @var string[] */
2932
public array $methods = [];
3033

3134
public function enterNode(Node $node)
3235
{
3336
if ($node instanceof Node\Stmt\Function_) {
37+
assert(isset($node->namespacedName));
38+
$functionName = $node->namespacedName->toLowerString();
39+
3440
foreach ($node->attrGroups as $attrGroup) {
3541
foreach ($attrGroup->attrs as $attr) {
36-
if ($attr->name->toString() === Pure::class) {
37-
$this->functions[] = $node->namespacedName->toLowerString();
42+
if ($attr->name->toString() !== Pure::class) {
43+
continue;
44+
}
45+
46+
// The following functions have side effects, but their state is managed within the PHPStan scope:
47+
if (in_array($functionName, [
48+
'stat',
49+
'lstat',
50+
'file_exists',
51+
'is_writable',
52+
'is_writeable',
53+
'is_readable',
54+
'is_executable',
55+
'is_file',
56+
'is_dir',
57+
'is_link',
58+
'filectime',
59+
'fileatime',
60+
'filemtime',
61+
'fileinode',
62+
'filegroup',
63+
'fileowner',
64+
'filesize',
65+
'filetype',
66+
'fileperms',
67+
'ftell',
68+
'ini_get',
69+
'function_exists',
70+
'json_last_error',
71+
'json_last_error_msg',
72+
], true)) {
73+
$this->functions[] = $functionName;
3874
break 2;
3975
}
76+
77+
// PhpStorm stub's #[Pure(true)] means the function has side effects but its return value is important.
78+
// In PHPStan's criteria, these functions are simply considered as ['hasSideEffect' => true].
79+
if (isset($attr->args[0]->value->name->name) && $attr->args[0]->value->name->name === 'true') {
80+
$this->impureFunctions[] = $functionName;
81+
} else {
82+
$this->functions[] = $functionName;
83+
}
84+
break 2;
4085
}
4186
}
4287
}
@@ -74,26 +119,29 @@ public function enterNode(Node $node)
74119
);
75120
}
76121

122+
/** @var array<string, array{hasSideEffects: bool}> $metadata */
77123
$metadata = require __DIR__ . '/functionMetadata_original.php';
78124
foreach ($visitor->functions as $functionName) {
79125
if (array_key_exists($functionName, $metadata)) {
80126
if ($metadata[$functionName]['hasSideEffects']) {
81-
if (in_array($functionName, [
82-
'mt_rand',
83-
'rand',
84-
'random_bytes',
85-
'random_int',
86-
'connection_aborted',
87-
'connection_status',
88-
'file_get_contents',
89-
], true)) {
90-
continue;
91-
}
92127
throw new ShouldNotHappenException($functionName);
93128
}
94129
}
95130
$metadata[$functionName] = ['hasSideEffects' => false];
96131
}
132+
foreach ($visitor->impureFunctions as $functionName) {
133+
if (array_key_exists($functionName, $metadata)) {
134+
if (in_array($functionName, [
135+
'ob_get_contents',
136+
], true)) {
137+
continue;
138+
}
139+
if ($metadata[$functionName]['hasSideEffects']) {
140+
throw new ShouldNotHappenException($functionName);
141+
}
142+
}
143+
$metadata[$functionName] = ['hasSideEffects' => true];
144+
}
97145

98146
foreach ($visitor->methods as $methodName) {
99147
if (array_key_exists($methodName, $metadata)) {

0 commit comments

Comments
 (0)