Skip to content

Commit df5c2a9

Browse files
committed
Mark by-ref out-parameter builtins as @pure-unless-parameter-passed
str_replace, str_ireplace, preg_replace, preg_match, preg_match_all and similar_text are pure unless their optional by-ref out parameter (count / matches / percent) receives an argument. Add the pureUnlessParameterPassedParameters metadata shape to the generator, the metadata schema test, and the metadata files.
1 parent cd8cc14 commit df5c2a9

4 files changed

Lines changed: 50 additions & 4 deletions

File tree

‎bin/functionMetadata_original.php‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
* the call is pure unless one of the listed callable parameters
1414
* (keyed by parameter name) receives an impure callable, e.g. array_map()
1515
* whose only side effects come from its 'callback' argument.
16+
* - ['pureUnlessParameterPassedParameters' => array<string, true>]
17+
* the call is pure unless one of the listed (by-ref out) parameters
18+
* (keyed by parameter name) receives an argument, e.g. str_replace()
19+
* whose only side effect is writing to its optional 'count' argument.
1620
*/
1721

18-
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
22+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}> */
1923
return [
2024
'abs' => ['hasSideEffects' => false],
2125
'acos' => ['hasSideEffects' => false],
@@ -264,14 +268,20 @@
264268
'output_reset_rewrite_vars' => ['hasSideEffects' => true],
265269
'pclose' => ['hasSideEffects' => true],
266270
'popen' => ['hasSideEffects' => true],
271+
'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]],
272+
'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]],
273+
'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
267274
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
275+
'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]],
268276
'readfile' => ['hasSideEffects' => true],
269277
'rename' => ['hasSideEffects' => true],
270278
'rewind' => ['hasSideEffects' => true],
271279
'rmdir' => ['hasSideEffects' => true],
272280
'sprintf' => ['hasSideEffects' => false],
273281
'str_decrement' => ['hasSideEffects' => false],
274282
'str_increment' => ['hasSideEffects' => false],
283+
'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
284+
'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
275285
'symlink' => ['hasSideEffects' => true],
276286
'time' => ['hasSideEffects' => true],
277287
'tempnam' => ['hasSideEffects' => true],

‎bin/generate-function-metadata.php‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public function enterNode(Node $node)
119119
);
120120
}
121121

122-
/** @var array<string, array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array<string, bool>}> $metadata */
122+
/** @var array<string, array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array<string, bool>, pureUnlessParameterPassedParameters?: array<string, bool>}> $metadata */
123123
$metadata = require __DIR__ . '/functionMetadata_original.php';
124124
foreach ($visitor->functions as $functionName) {
125125
if (array_key_exists($functionName, $metadata)) {
@@ -134,6 +134,14 @@ public function enterNode(Node $node)
134134

135135
continue;
136136
}
137+
138+
if (isset($metadata[$functionName]['pureUnlessParameterPassedParameters'])) {
139+
$metadata[$functionName] = [
140+
'pureUnlessParameterPassedParameters' => $metadata[$functionName]['pureUnlessParameterPassedParameters'],
141+
];
142+
143+
continue;
144+
}
137145
}
138146
$metadata[$functionName] = ['hasSideEffects' => false];
139147
}
@@ -192,9 +200,12 @@ public function enterNode(Node $node)
192200
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
193201
* one of the listed callable parameters (keyed by parameter name) receives an
194202
* impure callable, e.g. array_map()'s 'callback'.
203+
* - ['pureUnlessParameterPassedParameters' => array<string, true>] - pure unless
204+
* one of the listed (by-ref out) parameters (keyed by parameter name) receives
205+
* an argument, e.g. str_replace()'s 'replace_count'.
195206
*/
196207
197-
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
208+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}> */
198209
return [
199210
%s
200211
];
@@ -216,6 +227,20 @@ public function enterNode(Node $node)
216227
),
217228
),
218229
];
230+
$encodePureUnlessParameterPassedParameters = static fn (array $meta) => [
231+
$escape('pureUnlessParameterPassedParameters'),
232+
sprintf(
233+
'[%s]',
234+
implode(
235+
' ,',
236+
array_map(
237+
static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)),
238+
array_keys($meta['pureUnlessParameterPassedParameters']),
239+
$meta['pureUnlessParameterPassedParameters'],
240+
),
241+
),
242+
),
243+
];
219244

220245
foreach ($metadata as $name => $meta) {
221246
$content .= sprintf(
@@ -224,6 +249,7 @@ public function enterNode(Node $node)
224249
...match (true) {
225250
isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta),
226251
isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta),
252+
isset($meta['pureUnlessParameterPassedParameters']) => $encodePureUnlessParameterPassedParameters($meta),
227253
default => throw new ShouldNotHappenException($escape($meta)),
228254
},
229255
);

‎resources/functionMetadata.php‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
2020
* one of the listed callable parameters (keyed by parameter name) receives an
2121
* impure callable, e.g. array_map()'s 'callback'.
22+
* - ['pureUnlessParameterPassedParameters' => array<string, true>] - pure unless
23+
* one of the listed (by-ref out) parameters (keyed by parameter name) receives
24+
* an argument, e.g. str_replace()'s 'count'.
2225
*/
2326

24-
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
27+
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}> */
2528
return [
2629
'BackedEnum::from' => ['hasSideEffects' => false],
2730
'BackedEnum::tryFrom' => ['hasSideEffects' => false],
@@ -1634,7 +1637,10 @@
16341637
'preg_grep' => ['hasSideEffects' => false],
16351638
'preg_last_error' => ['hasSideEffects' => true],
16361639
'preg_last_error_msg' => ['hasSideEffects' => true],
1640+
'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]],
1641+
'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]],
16371642
'preg_quote' => ['hasSideEffects' => false],
1643+
'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
16381644
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
16391645
'preg_split' => ['hasSideEffects' => false],
16401646
'property_exists' => ['hasSideEffects' => false],
@@ -1666,6 +1672,7 @@
16661672
'rtrim' => ['hasSideEffects' => false],
16671673
'sha1' => ['hasSideEffects' => false],
16681674
'sha1_file' => ['hasSideEffects' => true],
1675+
'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]],
16691676
'sin' => ['hasSideEffects' => false],
16701677
'sinh' => ['hasSideEffects' => false],
16711678
'sizeof' => ['hasSideEffects' => false],
@@ -1680,8 +1687,10 @@
16801687
'str_ends_with' => ['hasSideEffects' => false],
16811688
'str_getcsv' => ['hasSideEffects' => false],
16821689
'str_increment' => ['hasSideEffects' => false],
1690+
'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
16831691
'str_pad' => ['hasSideEffects' => false],
16841692
'str_repeat' => ['hasSideEffects' => false],
1693+
'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
16851694
'str_rot13' => ['hasSideEffects' => false],
16861695
'str_split' => ['hasSideEffects' => false],
16871696
'str_starts_with' => ['hasSideEffects' => false],

‎tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public function testSchema(): void
2020
Expect::structure([
2121
'hasSideEffects' => Expect::bool(),
2222
'pureUnlessCallableIsImpureParameters' => Expect::arrayOf(Expect::bool(), Expect::string()),
23+
'pureUnlessParameterPassedParameters' => Expect::arrayOf(Expect::bool(), Expect::string()),
2324
])
2425
->assert(static fn ($v) => count((array) $v) > 0, 'Metadata entries must not be empty.')
2526
->required(),

0 commit comments

Comments
 (0)