Skip to content

Commit

Permalink
Fix preg_replace() return type
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored and ondrejmirtes committed Sep 2, 2024
1 parent 319e98b commit 0c9487d
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 5 deletions.
41 changes: 39 additions & 2 deletions src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
Expand All @@ -17,6 +18,7 @@
use PHPStan\Type\TypeUtils;
use function array_key_exists;
use function count;
use function in_array;

final class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
Expand Down Expand Up @@ -101,9 +103,30 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
if ($compareSuperTypes === $isStringSuperType) {
return new StringType();
} elseif ($compareSuperTypes === $isArraySuperType) {
if (count($subjectArgumentType->getArrays()) > 0) {
$subjectArrays = $subjectArgumentType->getArrays();
if (count($subjectArrays) > 0) {
$result = [];
foreach ($subjectArgumentType->getArrays() as $arrayType) {
foreach ($subjectArrays as $arrayType) {
$constantArrays = $arrayType->getConstantArrays();

if (
$constantArrays !== []
&& in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
) {
foreach ($constantArrays as $constantArray) {
$generalizedArray = $constantArray->generalizeValues();

$builder = ConstantArrayTypeBuilder::createEmpty();
// turn all keys optional
foreach ($constantArray->getKeyTypes() as $keyType) {
$builder->setOffsetValueType($keyType, $generalizedArray->getOffsetValueType($keyType), true);
}
$result[] = $builder->getArray();
}

continue;
}

$result[] = $arrayType->generalizeValues();
}

Expand Down Expand Up @@ -134,6 +157,20 @@ private function canReturnNull(
Scope $scope,
): bool
{
if (
in_array($functionReflection->getName(), ['preg_replace', 'preg_replace_callback', 'preg_replace_callback_array'], true)
&& count($functionCall->getArgs()) > 0
) {
$subjectArgumentType = $this->getSubjectType($functionReflection, $functionCall, $scope);

if (
$subjectArgumentType !== null
&& $subjectArgumentType->isArray()->yes()
) {
return false;
}
}

$possibleTypes = ParametersAcceptorSelector::selectFromArgs(
$scope,
$functionCall->getArgs(),
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7426,11 +7426,11 @@ public function dataReplaceFunctions(): array
'$expectedArray',
],
[
'array{a: string, b: string}|null',
'array{a?: string, b?: string}',
'$expectedArray2',
],
[
'array{a: string, b: string}|null',
'array{a?: string, b?: string}',
'$anotherExpectedArray',
],
[
Expand All @@ -7450,7 +7450,7 @@ public function dataReplaceFunctions(): array
'$anotherExpectedArrayOrString',
],
[
'array{a: string, b: string}|null',
'array{a?: string, b?: string}',
'preg_replace_callback_array($callbacks, $array)',
],
[
Expand Down
62 changes: 62 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11547.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Bug11547;

use function PHPStan\Testing\assertType;

function foo(string $s)
{
$r = preg_replace('/^a/', 'b', $s);
assertType('string|null', $r);
return $r;
}

function foobar(string $s, string $pattern)
{
$r = preg_replace($pattern, 'b', $s);
assertType('string|null', $r);
return $r;
}

/**
* @param array{a: string, b:string} $arr
*/
function bar(array $arr): array
{
$r = preg_replace('/^a/', 'x', $arr);
assertType('array{a?: string, b?: string}', $r);
return $r;
}

/**
* @param array{a: string, b:string} $arr
*/
function barbar($arr, string $pattern)
{
$r = preg_replace($pattern, 'b', $arr);
assertType('array{a?: string, b?: string}', $r);
return $r;
}

// see https://github.com/phpstan/phpstan/issues/11547#issuecomment-2307156443
// see https://3v4l.org/c70bG
function validPatternWithEmptyResult(string $s, array $arr) {
$r = preg_replace('/(\D+)*[12]/', 'x', $s);
assertType('string|null', $r);

$r = preg_replace('/(\D+)*[12]/', 'x', $arr);
assertType('array', $r);
}


/**
* @return string
*/
function fooCallback(string $s)
{
$r = preg_replace_callback('/^a/', function ($matches) {
return strtolower($matches[0]);
}, $s);
assertType('string|null', $r);
return $r;
}

0 comments on commit 0c9487d

Please sign in to comment.