diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index b297f3dc72..18a66da75c 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -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; @@ -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 { @@ -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(); } @@ -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(), diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 1b67568efa..1d17f9ee48 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -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', ], [ @@ -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)', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/bug-11547.php b/tests/PHPStan/Analyser/nsrt/bug-11547.php new file mode 100644 index 0000000000..3acb253f49 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11547.php @@ -0,0 +1,62 @@ +