diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 18a66da75c..c80c10d9c0 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -82,17 +83,26 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( return TypeUtils::toBenevolentUnion($defaultReturnType); } - if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { + if (array_key_exists($functionReflection->getName(), self::FUNCTIONS_REPLACE_POSITION)) { $replaceArgumentPosition = self::FUNCTIONS_REPLACE_POSITION[$functionReflection->getName()]; if (count($functionCall->getArgs()) > $replaceArgumentPosition) { $replaceArgumentType = $scope->getType($functionCall->getArgs()[$replaceArgumentPosition]->value); + $accessories = []; if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) { + $accessories[] = new AccessoryNonEmptyStringType(); } - if ($replaceArgumentType->isNonEmptyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + + if ($subjectArgumentType->isLowercaseString()->yes() && $replaceArgumentType->isLowercaseString()->yes()) { + $accessories[] = new AccessoryLowercaseStringType(); + } + + if (count($accessories) > 0) { + $accessories[] = new StringType(); + return new IntersectionType($accessories); } } } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 0cb675d14b..9860a71f59 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -7414,7 +7414,7 @@ public function dataReplaceFunctions(): array { return [ [ - 'non-falsy-string', + 'lowercase-string&non-falsy-string', '$expectedString', ], [ @@ -7422,7 +7422,7 @@ public function dataReplaceFunctions(): array '$expectedString2', ], [ - 'non-falsy-string|null', + '(lowercase-string&non-falsy-string)|null', '$anotherExpectedString', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php index ef291855ed..a38116b682 100644 --- a/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php +++ b/tests/PHPStan/Analyser/nsrt/isset-coalesce-empty-type.php @@ -472,7 +472,7 @@ function coalesce() assertType('int<0, max>', rand() ?? false); - assertType('0|string', preg_replace('', '', '') ?? 0); + assertType('0|lowercase-string', preg_replace('', '', '') ?? 0); $foo = new FooCoalesce(); diff --git a/tests/PHPStan/Analyser/nsrt/lowercase-string-replace.php b/tests/PHPStan/Analyser/nsrt/lowercase-string-replace.php new file mode 100644 index 0000000000..c9546ba2d0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/lowercase-string-replace.php @@ -0,0 +1,42 @@ +