Skip to content

Commit 0c9487d

Browse files
staabmondrejmirtes
authored andcommitted
Fix preg_replace() return type
1 parent 319e98b commit 0c9487d

File tree

3 files changed

+104
-5
lines changed

3 files changed

+104
-5
lines changed

src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\Reflection\ParametersAcceptorSelector;
99
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1010
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
11+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1112
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1213
use PHPStan\Type\IntersectionType;
1314
use PHPStan\Type\MixedType;
@@ -17,6 +18,7 @@
1718
use PHPStan\Type\TypeUtils;
1819
use function array_key_exists;
1920
use function count;
21+
use function in_array;
2022

2123
final class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2224
{
@@ -101,9 +103,30 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
101103
if ($compareSuperTypes === $isStringSuperType) {
102104
return new StringType();
103105
} elseif ($compareSuperTypes === $isArraySuperType) {
104-
if (count($subjectArgumentType->getArrays()) > 0) {
106+
$subjectArrays = $subjectArgumentType->getArrays();
107+
if (count($subjectArrays) > 0) {
105108
$result = [];
106-
foreach ($subjectArgumentType->getArrays() as $arrayType) {
109+
foreach ($subjectArrays as $arrayType) {
110+
$constantArrays = $arrayType->getConstantArrays();
111+
112+
if (
113+
$constantArrays !== []
114+
&& in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
115+
) {
116+
foreach ($constantArrays as $constantArray) {
117+
$generalizedArray = $constantArray->generalizeValues();
118+
119+
$builder = ConstantArrayTypeBuilder::createEmpty();
120+
// turn all keys optional
121+
foreach ($constantArray->getKeyTypes() as $keyType) {
122+
$builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true);
123+
}
124+
$result[] = $builder->getArray();
125+
}
126+
127+
continue;
128+
}
129+
107130
$result[] = $arrayType->generalizeValues();
108131
}
109132

@@ -134,6 +157,20 @@ private function canReturnNull(
134157
Scope $scope,
135158
): bool
136159
{
160+
if (
161+
in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
162+
&& count($functionCall->getArgs()) > 0
163+
) {
164+
$subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope);
165+
166+
if (
167+
$subjectArgumentType !== null
168+
&& $subjectArgumentType->isArray()->yes()
169+
) {
170+
return false;
171+
}
172+
}
173+
137174
$possibleTypes = ParametersAcceptorSelector::selectFromArgs(
138175
$scope,
139176
$functionCall->getArgs(),

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -7426,11 +7426,11 @@ public function dataReplaceFunctions(): array
74267426
'$expectedArray',
74277427
],
74287428
[
7429-
'array{a: string, b: string}|null',
7429+
'array{a?: string, b?: string}',
74307430
'$expectedArray2',
74317431
],
74327432
[
7433-
'array{a: string, b: string}|null',
7433+
'array{a?: string, b?: string}',
74347434
'$anotherExpectedArray',
74357435
],
74367436
[
@@ -7450,7 +7450,7 @@ public function dataReplaceFunctions(): array
74507450
'$anotherExpectedArrayOrString',
74517451
],
74527452
[
7453-
'array{a: string, b: string}|null',
7453+
'array{a?: string, b?: string}',
74547454
'preg_replace_callback_array($callbacks, $array)',
74557455
],
74567456
[
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Bug11547;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function foo(string $s)
8+
{
9+
$r = preg_replace('/^a/', 'b', $s);
10+
assertType('string|null', $r);
11+
return $r;
12+
}
13+
14+
function foobar(string $s, string $pattern)
15+
{
16+
$r = preg_replace($pattern, 'b', $s);
17+
assertType('string|null', $r);
18+
return $r;
19+
}
20+
21+
/**
22+
* @param array{a: string, b:string} $arr
23+
*/
24+
function bar(array $arr): array
25+
{
26+
$r = preg_replace('/^a/', 'x', $arr);
27+
assertType('array{a?: string, b?: string}', $r);
28+
return $r;
29+
}
30+
31+
/**
32+
* @param array{a: string, b:string} $arr
33+
*/
34+
function barbar($arr, string $pattern)
35+
{
36+
$r = preg_replace($pattern, 'b', $arr);
37+
assertType('array{a?: string, b?: string}', $r);
38+
return $r;
39+
}
40+
41+
// see https://github.com/phpstan/phpstan/issues/11547#issuecomment-2307156443
42+
// see https://3v4l.org/c70bG
43+
function validPatternWithEmptyResult(string $s, array $arr) {
44+
$r = preg_replace('/(\D+)*[12]/', 'x', $s);
45+
assertType('string|null', $r);
46+
47+
$r = preg_replace('/(\D+)*[12]/', 'x', $arr);
48+
assertType('array', $r);
49+
}
50+
51+
52+
/**
53+
* @return string
54+
*/
55+
function fooCallback(string $s)
56+
{
57+
$r = preg_replace_callback('/^a/', function ($matches) {
58+
return strtolower($matches[0]);
59+
}, $s);
60+
assertType('string|null', $r);
61+
return $r;
62+
}

0 commit comments

Comments
 (0)