diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c9d9f6339f0..691b77ec5a0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2023,18 +2023,7 @@ public function enterAnonymousFunctionWithoutReflection( $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { - if (isset($callableParameters[$i])) { - $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); - } elseif (count($callableParameters) > 0) { - $lastParameter = array_last($callableParameters); - if ($lastParameter->isVariadic()) { - $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); - } else { - $parameterType = self::intersectButNotNever($parameterType, new MixedType()); - } - } else { - $parameterType = self::intersectButNotNever($parameterType, new MixedType()); - } + $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i)); } $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); $expressionTypes[$paramExprString] = $holder; @@ -2233,20 +2222,8 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun foreach ($arrowFunction->params as $i => $parameter) { $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); - if ($callableParameters !== null) { - if (isset($callableParameters[$i])) { - $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType()); - } elseif (count($callableParameters) > 0) { - $lastParameter = array_last($callableParameters); - if ($lastParameter->isVariadic()) { - $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType()); - } else { - $parameterType = self::intersectButNotNever($parameterType, new MixedType()); - } - } else { - $parameterType = self::intersectButNotNever($parameterType, new MixedType()); - } + $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i)); } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { @@ -2312,6 +2289,27 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this)); } + /** + * @param ParameterReflection[] $callableParameters + */ + private function getCallableParameterType(array $callableParameters, int $index): Type + { + if (isset($callableParameters[$index])) { + return $callableParameters[$index]->getType(); + } + + if (count($callableParameters) === 0) { + return new MixedType(); + } + + $lastParameter = array_last($callableParameters); + if ($lastParameter->isVariadic()) { + return $lastParameter->getType(); + } + + return new MixedType(); + } + public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type { if ($nativeType->isSuperTypeOf($inferredType)->no()) { diff --git a/src/Type/ConditionalType.php b/src/Type/ConditionalType.php index f154fb5368a..3819be25545 100644 --- a/src/Type/ConditionalType.php +++ b/src/Type/ConditionalType.php @@ -113,7 +113,13 @@ public function describe(VerbosityLevel $level): string public function isResolvable(): bool { - return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target); + if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) { + return true; + } + + $isSuperType = $this->target->isSuperTypeOf($this->subject); + + return $isSuperType->yes() || $isSuperType->no(); } protected function getResult(): Type diff --git a/tests/PHPStan/Analyser/nsrt/bug-11894.php b/tests/PHPStan/Analyser/nsrt/bug-11894.php new file mode 100644 index 00000000000..cb91f1929c1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11894.php @@ -0,0 +1,70 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11894Nsrt; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param T $a + * @return (T is string ? string : T) + */ +function conditionalReturn(mixed $a): mixed +{ + if (!is_string($a)) { + return $a; + } + return trim($a); +} + +/** + * @template T of string|null + * @param T $a + */ +function testNarrowedToString(mixed $a): void +{ + if (!is_string($a)) { + return; + } + assertType('string', conditionalReturn($a)); +} + +/** + * @template T of int|null + * @param T $a + */ +function testNarrowedToNonMatchingType(mixed $a): void +{ + if (!is_int($a)) { + return; + } + assertType('T of int (function Bug11894Nsrt\testNarrowedToNonMatchingType(), argument)', conditionalReturn($a)); +} + +/** + * @template T of string|int + * @param T $a + */ +function testNotFullyNarrowable(mixed $a): void +{ + assertType('string|T of int (function Bug11894Nsrt\testNotFullyNarrowable(), argument)', conditionalReturn($a)); +} + +abstract class ConditionalArrayKeys +{ + /** + * @template TKey of array-key + * @template TArray of array + * @param TArray $array + * @return (TArray is non-empty-array ? non-empty-list : list) + */ + abstract public function arrayKeys(array $array): array; + + /** @param non-empty-array $nonEmpty */ + public function testMaybeStaysUnresolved(array $nonEmpty): void + { + assertType('non-empty-list', $this->arrayKeys($nonEmpty)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8048.php b/tests/PHPStan/Analyser/nsrt/bug-8048.php new file mode 100644 index 00000000000..504605669c2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8048.php @@ -0,0 +1,35 @@ +|null $responseType + * + * @return ($responseType is class-string ? T : null) + */ + public function request(?string $responseType = null): ?CustomResponseInterface + { + if ($responseType === null) { + return null; + } + + return new CustomResponse(); + } +} + +function (): void { + assertType('null', (new ApiService())->request(null)); + assertType('Bug8048Nsrt\CustomResponse', (new ApiService())->request(CustomResponse::class)); + $x = rand(0, 1) ? CustomResponse::class : null; + assertType('Bug8048Nsrt\CustomResponse|null', (new ApiService())->request($x)); +}; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 1b62fe7e8b8..f5b3ad7b2f0 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2956,4 +2956,9 @@ public function testBug3842(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []); } + public function testBug11894(): void + { + $this->analyse([__DIR__ . '/data/bug-11894.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11894.php b/tests/PHPStan/Rules/Functions/data/bug-11894.php new file mode 100644 index 00000000000..798919a5d6e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11894.php @@ -0,0 +1,79 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-11894.php'], []); + } + + public function testBug8048(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-8048.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 751fc0b8771..076e4770e18 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1031,4 +1031,10 @@ public function testConstantParameterCheckStatic(): void ]); } + public function testBug11894(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-11894.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-11894.php b/tests/PHPStan/Rules/Methods/data/bug-11894.php new file mode 100644 index 00000000000..7b4daf0ada4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11894.php @@ -0,0 +1,81 @@ +conditionalReturn($a); + } + + /** + * @template T of string|null + * @param T $a + */ + public function testStaticMethod(mixed $a): mixed + { + if (!is_string($a)) { + return $a; + } + + return Converter::conditionalReturnStatic($a); + } + + /** + * @template T of string|int + * @param T $a + */ + public function testMaybeMethod(mixed $a): mixed + { + $c = new Converter(); + return $c->conditionalReturn($a); + } + + /** + * @template T of string|int + * @param T $a + */ + public function testMaybeStaticMethod(mixed $a): mixed + { + return Converter::conditionalReturnStatic($a); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8048.php b/tests/PHPStan/Rules/Methods/data/bug-8048.php new file mode 100644 index 00000000000..d4f7e7831ea --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8048.php @@ -0,0 +1,33 @@ +|null $responseType + * + * @return ($responseType is class-string ? T : null) + */ + public function request(?string $responseType = null): ?CustomResponseInterface + { + if ($responseType === null) { + return null; + } + + return new CustomResponse(); + } +} + +function (): void { + (new ApiService())->request(null); + (new ApiService())->request(CustomResponse::class); + $x = rand(0, 1) ? CustomResponse::class : null; + (new ApiService())->request($x); +};