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 3da95192432..1860256abb2 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2871,4 +2871,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 85937c21b02..1b82fd6ba3a 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1015,4 +1015,10 @@ public function testPipeOperator(): 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); +};