diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f4f74bff5cc..b02a54e6b0f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2621,6 +2621,12 @@ public static function intersectButNotNever(Type $nativeType, Type $inferredType } $result = TypeCombinator::intersect($nativeType, $inferredType); + if ($result instanceof NeverType) { + // the inferred type says no value is ever produced - the native + // type's nullability must not resurrect one + return $result; + } + if (TypeCombinator::containsNull($nativeType)) { return TypeCombinator::addNull($result); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15285.php b/tests/PHPStan/Analyser/nsrt/bug-15285.php new file mode 100644 index 00000000000..622324460e0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15285.php @@ -0,0 +1,99 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug15285; + +use Closure; +use Generator; +use LogicException; +use RuntimeException; +use function PHPStan\Testing\assertType; + +final class CartController +{ + + public function redirect(string $destination): never + { + throw new RuntimeException($destination); + } + + /** + * @param Closure(string, (Closure(): never)|null=): never $errorCallback + */ + public function addToCart(bool $isAvailable, Closure $errorCallback): void + { + if ($isAvailable) { + return; + } + + $errorCallback( + 'This offer is no longer available.', + fn (): null => $this->redirect('/product'), + ); + } + + public function dump(): void + { + assertType('Closure(): never', fn () => $this->redirect('/product')); + assertType('Closure(): never', fn (): null => $this->redirect('/product')); + assertType('Closure(): never', fn (): int => throw new LogicException()); + assertType('Closure(): never', fn (): ?int => throw new LogicException()); + + assertType('Closure(): never', function () { + $this->redirect('/product'); + }); + assertType('Closure(): never', function (): null { + $this->redirect('/product'); + }); + assertType('Closure(): never', function (): ?int { + throw new LogicException(); + }); + + assertType('static-Closure(): never', static fn (): ?int => throw new LogicException()); + assertType('static-Closure(): never', static function (): null { + throw new LogicException(); + }); + + assertType('Closure(): never', function (): ?Generator { + throw new LogicException(); + }); + assertType('Closure(): Generator', function (): ?Generator { + yield 1; + }); + } + +} + +/** + * @param list $emptyList + */ +function neverCallableParameters(array $emptyList): void +{ + array_map(function (?int $i): void { + assertType('never', $i); + }, $emptyList); + + array_map(fn (?int $i) => assertType('never', $i), $emptyList); + + array_map(function (null $i): void { + assertType('never', $i); + }, $emptyList); +} + +/** + * @param callable(never...): void $cb + */ +function neverVariadicCallable(callable $cb): void +{ +} + +function neverVariadicCallableParameters(): void +{ + neverVariadicCallable(function (?int $a, ?string $b): void { + assertType('never', $a); + assertType('never', $b); + }); + + neverVariadicCallable(fn (?int $a) => assertType('never', $a)); +} diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 67d0c0adc28..536bd63a807 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -449,6 +449,12 @@ public function testBug15251(): void $this->analyse([__DIR__ . '/../Methods/data/bug-15251.php'], []); } + #[RequiresPhp('>= 8.2.0')] + public function testBug15285(): void + { + $this->analyse([__DIR__ . '/data/bug-15285.php'], []); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11935WithCheckExplicitMixed(): void { diff --git a/tests/PHPStan/Rules/Functions/data/bug-15285.php b/tests/PHPStan/Rules/Functions/data/bug-15285.php new file mode 100644 index 00000000000..f8764ff7ec4 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15285.php @@ -0,0 +1,33 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug15285CallCallables; + +use Closure; +use RuntimeException; + +final class CartController +{ + + public function redirect(string $destination): never + { + throw new RuntimeException($destination); + } + + /** + * @param Closure(string, (Closure(): never)|null=): never $errorCallback + */ + public function addToCart(bool $isAvailable, Closure $errorCallback): void + { + if ($isAvailable) { + return; + } + + $errorCallback( + 'This offer is no longer available.', + fn (): null => $this->redirect('/product'), + ); + } + +}