diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8ff656bb57..667445d34d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5139,16 +5139,22 @@ private function processArgs( $scopeToPass = $closureBindScope; } + $parameterCallableType = null; + if ($parameterType !== null) { + $parameterCallableType = TypeUtils::findCallableType($parameterType); + } + if ($parameter instanceof ExtendedParameterReflection) { $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; } else { $callCallbackImmediately = $parameterCallImmediately->yes(); } } else { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $parameterCallableType !== null && $calleeReflection instanceof FunctionReflection; } + if ($arg->value instanceof Expr\Closure) { $restoreThisScope = null; if ( diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 7213a8140f..d649cfe0a7 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -170,6 +170,24 @@ public static function findThisType(Type $type): ?ThisType return null; } + public static function findCallableType(Type $type): ?Type + { + if ($type->isCallable()->yes()) { + return $type; + } + + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + $callableType = self::findCallableType($innerType); + if ($callableType !== null) { + return $callableType; + } + } + } + + return null; + } + /** * @return HasPropertyType[] */ diff --git a/stubs/core.stub b/stubs/core.stub index de4c904423..6456c2d1b9 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -327,3 +327,38 @@ function get_defined_constants(bool $categorize = false): array {} */ function getopt(string $short_options, array $long_options = [], &$rest_index = null) {} +/** + * @param callable|int $handler + * @param-later-invoked-callable $handler + */ +function pcntl_signal(int $signal, $handler, bool $restart_syscalls = true): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function set_error_handler(?callable $callback, int $error_levels = E_ALL): ?callable {} + +/** + * @param-later-invoked-callable $callback + */ +function set_exception_handler(?callable $callback): ?callable {} + +/** + * @param-later-invoked-callable $callback + */ +function spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function register_shutdown_function(callable $callback, mixed ...$args): void {} + +/** + * @param-later-invoked-callable $callback + */ +function header_register_callback(callable $callback): bool {} + +/** + * @param-later-invoked-callable $callback + */ +function register_tick_function(callable $callback, mixed ...$args): bool {} diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index dbca93c756..5012b0ef87 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -7,7 +7,9 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPUnit\Framework\Attributes\DataProvider; use function count; use function get_class; @@ -127,6 +129,22 @@ public static function dataIsAlwaysTerminating(): array 'var_dump(exit()."a");', true, ], + [ + 'array_push($arr, fn() => "exit");', + false, + ], + [ + 'array_push($arr, function() { exit(); });', + false, + ], + [ + 'array_push($arr, "exit");', + false, + ], + [ + 'array_unshift($arr, "exit");', + false, + ], ]; } @@ -155,7 +173,8 @@ public function testIsAlwaysTerminating( /** @var ScopeFactory $scopeFactory */ $scopeFactory = self::getContainer()->getByType(ScopeFactory::class); $scope = $scopeFactory->create(ScopeContext::create('test.php')) - ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()); + ->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes()) + ->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes()); $result = $nodeScopeResolver->processExprNode( $stmt, diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index aadd868de8..e79a185935 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -348,4 +348,29 @@ public function testBug13232d(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13288(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13288.php'], []); + } + + public function testBug13311(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13311.php'], []); + } + + public function testBug13307(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13307.php'], []); + } + + public function testBug13331(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13331.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13288.php b/tests/PHPStan/Rules/DeadCode/data/bug-13288.php new file mode 100644 index 0000000000..2285288f93 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13288.php @@ -0,0 +1,11 @@ += 8.1 + +namespace Bug13288; + +function error_to_exception(int $errno, string $errstr, string $errfile = 'unknown', int $errline = 0): never { + throw new \ErrorException($errstr, $errno, $errno, $errfile, $errline); +} + +set_error_handler(error_to_exception(...)); + +echo 'ok'; diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13307.php b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php new file mode 100644 index 0000000000..78dc84dd4b --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php @@ -0,0 +1,24 @@ +analyse([__DIR__ . '/data/bug-13201.php'], []); } + public function testBug12119(): void + { + $this->analyse([__DIR__ . '/data/bug-12119.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/bug-12119.php b/tests/PHPStan/Rules/Pure/data/bug-12119.php new file mode 100644 index 0000000000..1196daf7ee --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-12119.php @@ -0,0 +1,21 @@ +testMethod('random_int'); + $b = testFunction('random_int'); + + return $a . $b; + } +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php index 6a4bb319b3..295e0c364d 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-function.php +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -164,3 +164,88 @@ function justContainsInlineHtml() exit() // ok, as array_push() will not invoke the function + ); + + $exitingClosure = function () { + exit(); + }; + array_push($a, // error because by ref arg + $exitingClosure // ok, as array_push() will not invoke the function + ); + + takesString("exit"); // ok, as the maybe callable type string is not typed with immediately-invoked-callable +} + +/** @phpstan-pure */ +function takesString(string $s) { +} + +/** @phpstan-pure */ +function bug13288b() +{ + $exitingClosure = function () { + exit(); + }; + + takesMixed($exitingClosure); // error because immediately invoked +} + +/** + * @phpstan-pure + * @param-immediately-invoked-callable $m + */ +function takesMixed(mixed $m) { +} + +/** @phpstan-pure */ +function bug13288c() +{ + $exitingClosure = function () { + exit(); + }; + + takesMaybeCallable($exitingClosure); +} + +/** @phpstan-pure */ +function takesMaybeCallable(?callable $c) { // arguments passed to functions are considered "immediately called" by default +} + +/** @phpstan-pure */ +function bug13288d() +{ + $exitingClosure = function () { + exit(); + }; + takesMaybeCallable2($exitingClosure); +} + +/** @phpstan-pure */ +function takesMaybeCallable2(?\Closure $c) { // Closures are considered "immediately called" +} + +/** @phpstan-pure */ +function bug13288e(MyClass $m) +{ + $exitingClosure = function () { + exit(); + }; + $m->takesMaybeCallable($exitingClosure); +} + +class MyClass { + /** @phpstan-pure */ + function takesMaybeCallable(?callable $c) { // arguments passed to methods are considered "later called" by default + } +} +