diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 718eb63e3c9..6f8731e5ad1 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1584,6 +1584,15 @@ private function getConditionalSpecifiedTypes( $ifType = $conditionalType->getIf(); $elseType = $conditionalType->getElse(); + if ( + ( + $argumentExpr instanceof Node\Scalar + || ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true)) + ) && ($ifType instanceof NeverType || $elseType instanceof NeverType) + ) { + return null; + } + if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) { $context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue(); } elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) { diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 0a399fdc3c6..f67114ce681 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1179,4 +1179,19 @@ public function testBug14177(): void $this->analyse([__DIR__ . '/data/bug-14177.php'], []); } + public function testBug13566(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13566.php'], [ + [ + 'Call to function is_numeric() with 123 will always evaluate to true.', + 199, + ], + [ + 'Call to function assertIsInt() with int will always evaluate to true.', + 204, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index fb64511afff..91371852a16 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -290,6 +290,18 @@ public function testBug12087b(): void ]); } + public function testBug13566(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13566.php'], []); + } + + public function testBug10337(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10337.php'], []); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 8a680853799..efd534dda7e 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -161,6 +161,12 @@ public function testBug12087b(): void ]); } + public function testBug13566(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13566.php'], []); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10337.php b/tests/PHPStan/Rules/Comparison/data/bug-10337.php new file mode 100644 index 00000000000..a0efb9ca9a5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10337.php @@ -0,0 +1,62 @@ +callExit(true); + } + + /** + * @return never + */ + public function testVoidAndNever(): void + { + $app = new App(); + assertType('null', $app->callExit(true)); + assertType('never', $app->callExit(false)); + } + + /** + * @return never + */ + public function testVoidAndNever2(): void + { + $app = new class() extends App { + }; + assertType('null', $app->callExit(true)); + assertType('never', $app->callExit(false)); + } + + /** + * @return never + */ + public function testVoidAndNever3(): void + { + $app = new class() extends App { + #[\Override] + public function callExit(bool $calledFromShutdownHandler = false): void + { + parent::callExit($calledFromShutdownHandler); + } + }; + assertType('null', $app->callExit(true)); + assertType('never', $app->callExit(false)); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13566.php b/tests/PHPStan/Rules/Comparison/data/bug-13566.php new file mode 100644 index 00000000000..0f0ac5a4b4d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13566.php @@ -0,0 +1,212 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13566; + +use LogicException; + +class ReturnViaBool +{ + /** @return ($exit is true ? never : void) */ + public static function notFound(bool $exit = true): void + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit) { + echo '404 Not Found'; + exit; + } + } + + public function test(): void + { + // send 404 header without exiting + self::notFound(false); + } +} + +class ReturnViaInt +{ + /** @return ($exit is 1 ? never : void) */ + public static function notFound(int $exit = 1): void + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit == 1) { + echo '404 Not Found'; + exit; + } + } + + public function test(): void + { + // send 404 header + self::notFound(0); + } +} + +class ReturnViaString +{ + /** @return ($exit is '1' ? never : void) */ + public static function notFound(string $exit = '1'): void + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit == '1') { + echo '404 Not Found'; + exit; + } + } + + public function test(): void + { + // send 404 header + self::notFound('0'); + } +} + +class ReturnViaIntNonNative +{ + /** @return ($exit is 1 ? never : void) */ + public static function notFound(int $exit = 1) + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit == 1) { + echo '404 Not Found'; + exit; + } + } + + public function test(): void + { + // send 404 header + self::notFound(0); + } +} + +class ReturnsMaybeNever +{ + /** @return ($exit is true ? never : 1) */ + public static function notFound(bool $exit = true) + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit) { + echo '404 Not Found'; + exit; + } + return 1; + } + + public function test(): void + { + // send 404 header + self::notFound(false); + + } +} + +class ReturnsMaybeVoid +{ + /** @return ($exit is true ? void : 1) */ + public static function notFound(bool $exit = true) + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit) { + echo '404 Not Found'; + return; + } + return 1; + } + + public function test(): void + { + // send 404 header + self::notFound(false); + + } +} + + + +class ReturnsWithInstanceMethod +{ + /** @return ($exit is true ? never-return : void) */ + public function notFound(bool $exit = true): void + { + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit) { + echo '404 Not Found'; + exit; + } + } + + + public function test(): void + { + // send 404 header + $this->notFound(false); + + } +} + +/** @return ($exit is true ? never-return : void) */ +function notFound(bool $exit = true): void +{ + header('HTTP/1.1 404 Not Found', true, 404); + + if ($exit) { + echo '404 Not Found'; + exit; + } +} + +function test(): void +{ + // send 404 header + notFound(false); +} + + +class ReturnViaUnion +{ + /** + * @return ($exit is int ? void : never) + */ + public static function notFound(int|string $exit = 'hello world'): void + { + header('HTTP/1.1 404 Not Found', true, 404); + + if (!is_int($exit)) { + echo '404 Not Found'; + exit; + } + } + + public function test(): void + { + // send 404 header + self::notFound(0); + } +} + + +$x = 123; +if (is_numeric($x)) { +} + +function doFoo($mixed) { + assertIsInt($mixed); + assertIsInt($mixed); +} + +/** @phpstan-assert int $i */ +function assertIsInt($i):void { + if (!is_int($i)) { + throw new \LogicException(); + } +}