From b8bbcc005cebc77170934c2e6ad2c99de407f010 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 11:46:46 +0200 Subject: [PATCH 01/20] Fix args are mistakenly handled as immediately-invoked --- src/Analyser/NodeScopeResolver.php | 9 +++++++- .../PHPStan/Analyser/ExpressionResultTest.php | 21 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8ff656bb57..06349dc059 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5139,7 +5139,13 @@ private function processArgs( $scopeToPass = $closureBindScope; } - if ($parameter instanceof ExtendedParameterReflection) { + if ( + $parameterType === null + || $parameterType instanceof MixedType + || $parameterType->isCallable()->no() + ) { + $callCallbackImmediately = false; + } elseif ($parameter instanceof ExtendedParameterReflection) { $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); if ($parameterCallImmediately->maybe()) { $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; @@ -5149,6 +5155,7 @@ private function processArgs( } else { $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; } + if ($arg->value instanceof Expr\Closure) { $restoreThisScope = null; if ( 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, From 58edcdb4f05a40d0acf05fc668a1164e70b1f485 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 12:06:45 +0200 Subject: [PATCH 02/20] Update missing-exception-function-throws.php --- .../Exceptions/data/missing-exception-function-throws.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php index fa699f6bd7..443b716b94 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-function-throws.php @@ -56,3 +56,10 @@ function doBar3(): void { throw new \LogicException(); // error } + +function bug13288(array $a): void +{ + array_push($a, function() { + throw new \LogicException(); // ok, as array_push() will not invoke the function + }); +} From a6530cf304ea739a6be9798163fb257075c41f0d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 12:11:01 +0200 Subject: [PATCH 03/20] test impurePoints --- tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php | 4 ++++ tests/PHPStan/Rules/Pure/data/pure-function.php | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 66d1cec464..0f5e0dd4ad 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -104,6 +104,10 @@ public function testRule(): void 'Impure output between PHP opening and closing tags in pure function PureFunction\justContainsInlineHtml().', 160, ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 171, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php index 6a4bb319b3..ba2242a8e9 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-function.php +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -164,3 +164,11 @@ function justContainsInlineHtml() Date: Tue, 22 Jul 2025 12:18:17 +0200 Subject: [PATCH 04/20] more pureness tests for all code paths --- tests/PHPStan/Rules/Pure/data/pure-function.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php index ba2242a8e9..44901193d2 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-function.php +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -171,4 +171,16 @@ function bug13288(array $a) array_push($a, function() { // error because by ref arg exit(); // ok, as array_push() will not invoke the function }); + + array_push($a, // error because by ref arg + fn() => exit() // ok, as array_push() will not invoke the function + ); + + $closure = function() { + exit(); + }; + array_push($a, // error because by ref arg + $closure // ok, as array_push() will not invoke the function + ); } + From 8369e59d15a6e17f0f6268039727361630720f92 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 12:19:53 +0200 Subject: [PATCH 05/20] Update PureFunctionRuleTest.php --- tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 0f5e0dd4ad..dd2dfe3b96 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -108,6 +108,14 @@ public function testRule(): void 'Impure call to function array_push() in pure function PureFunction\bug13288().', 171, ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 175, + ], + [ + 'Impure call to function array_push() in pure function PureFunction\bug13288().', + 182, + ], ]); } From 12384029ec4ddf89ff08097adcdb926e86960a06 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 23 Jul 2025 09:19:00 +0200 Subject: [PATCH 06/20] Don't handle native functions as immediately-invoked by default the php engine might invoke the callables whenever it wants. the assumption about a function invoking the callable immediately fits for userland functions good enough though --- src/Analyser/NodeScopeResolver.php | 12 +++--------- .../Rules/DeadCode/UnreachableStatementRuleTest.php | 7 +++++++ tests/PHPStan/Rules/DeadCode/data/bug-13288.php | 11 +++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13288.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 06349dc059..a43391680e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5139,21 +5139,15 @@ private function processArgs( $scopeToPass = $closureBindScope; } - if ( - $parameterType === null - || $parameterType instanceof MixedType - || $parameterType->isCallable()->no() - ) { - $callCallbackImmediately = false; - } elseif ($parameter instanceof ExtendedParameterReflection) { + if ($parameter instanceof ExtendedParameterReflection) { $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection && !$calleeReflection->isBuiltin(); } else { $callCallbackImmediately = $parameterCallImmediately->yes(); } } else { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection && !$calleeReflection->isBuiltin(); } if ($arg->value instanceof Expr\Closure) { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index aadd868de8..1f218ca1bb 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -348,4 +348,11 @@ public function testBug13232d(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13288(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13288.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'; From 381d078f0de2eb5dab782e33d08c00ce717fe716 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Jul 2025 10:01:18 +0200 Subject: [PATCH 07/20] Added regression test --- .../DeadCode/UnreachableStatementRuleTest.php | 6 ++++++ tests/PHPStan/Rules/DeadCode/data/bug-13311.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13311.php diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 1f218ca1bb..a4bbe26fae 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -355,4 +355,10 @@ public function testBug13288(): void $this->analyse([__DIR__ . '/data/bug-13288.php'], []); } + public function testBug13311(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13311.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13311.php b/tests/PHPStan/Rules/DeadCode/data/bug-13311.php new file mode 100644 index 0000000000..4e80ca4902 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13311.php @@ -0,0 +1,14 @@ + Date: Wed, 30 Jul 2025 15:11:02 +0200 Subject: [PATCH 08/20] added regression test --- .../Rules/DeadCode/UnreachableStatementRuleTest.php | 6 ++++++ tests/PHPStan/Rules/DeadCode/data/bug-13331.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13331.php diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index a4bbe26fae..d83c65e4ee 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -361,4 +361,10 @@ public function testBug13311(): void $this->analyse([__DIR__ . '/data/bug-13311.php'], []); } + public function testBug13331(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13331.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13331.php b/tests/PHPStan/Rules/DeadCode/data/bug-13331.php new file mode 100644 index 0000000000..0aa836eb67 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13331.php @@ -0,0 +1,12 @@ + Date: Thu, 31 Jul 2025 12:27:27 +0200 Subject: [PATCH 09/20] fix --- src/Analyser/NodeScopeResolver.php | 12 +++++++++--- stubs/core.stub | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a43391680e..06349dc059 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5139,15 +5139,21 @@ private function processArgs( $scopeToPass = $closureBindScope; } - if ($parameter instanceof ExtendedParameterReflection) { + if ( + $parameterType === null + || $parameterType instanceof MixedType + || $parameterType->isCallable()->no() + ) { + $callCallbackImmediately = false; + } elseif ($parameter instanceof ExtendedParameterReflection) { $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection && !$calleeReflection->isBuiltin(); + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; } else { $callCallbackImmediately = $parameterCallImmediately->yes(); } } else { - $callCallbackImmediately = $calleeReflection instanceof FunctionReflection && !$calleeReflection->isBuiltin(); + $callCallbackImmediately = $calleeReflection instanceof FunctionReflection; } if ($arg->value instanceof Expr\Closure) { diff --git a/stubs/core.stub b/stubs/core.stub index de4c904423..16aceff07a 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -327,3 +327,14 @@ 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 + * @return string|array|object|null + */ +function set_error_handler(?callable $callback, int $error_levels = E_ALL) {} From 068adb89a954945d9a6a4a47fb6bcc4a1dcd1aa2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 12:30:23 +0200 Subject: [PATCH 10/20] Update core.stub --- stubs/core.stub | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stubs/core.stub b/stubs/core.stub index 16aceff07a..982cd4e16b 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -335,6 +335,5 @@ function pcntl_signal(int $signal, $handler, bool $restart_syscalls = true): boo /** * @param-later-invoked-callable $callback - * @return string|array|object|null */ -function set_error_handler(?callable $callback, int $error_levels = E_ALL) {} +function set_error_handler(?callable $callback, int $error_levels = E_ALL): ?callable {} From bec39e5089a00b87a7acf99b9bf6a1b594aa88ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 12:33:25 +0200 Subject: [PATCH 11/20] Update core.stub --- stubs/core.stub | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stubs/core.stub b/stubs/core.stub index 982cd4e16b..2f66ae8d50 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -337,3 +337,13 @@ function pcntl_signal(int $signal, $handler, bool $restart_syscalls = true): boo * @param-later-invoked-callable $callback */ function set_error_handler(?callable $callback, int $error_levels = E_ALL): ?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 {} From 8119947bb8fb73fdd09d4e0d0ff518b1ed1b7244 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 12:35:03 +0200 Subject: [PATCH 12/20] Update core.stub --- stubs/core.stub | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stubs/core.stub b/stubs/core.stub index 2f66ae8d50..9ad87c10d8 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -338,6 +338,11 @@ function pcntl_signal(int $signal, $handler, bool $restart_syscalls = true): boo */ 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 */ From b2822bd9540128533d1e8dedd95cc62f4e838d3c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 12:38:29 +0200 Subject: [PATCH 13/20] Update core.stub --- stubs/core.stub | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stubs/core.stub b/stubs/core.stub index 9ad87c10d8..b131bbf8d4 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -352,3 +352,8 @@ function spl_autoload_register(?callable $callback = null, bool $throw = true, b * @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 {} From d11ab4285c1a7fabbf3e26db57f9e0addde585eb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 12:42:05 +0200 Subject: [PATCH 14/20] Update core.stub --- stubs/core.stub | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stubs/core.stub b/stubs/core.stub index b131bbf8d4..6456c2d1b9 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -357,3 +357,8 @@ 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 {} From 9b938c30db0e0039a77e83c863c6da2acd8e77c4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 31 Jul 2025 13:01:54 +0200 Subject: [PATCH 15/20] Added regression test --- .../DeadCode/UnreachableStatementRuleTest.php | 6 +++++ .../PHPStan/Rules/DeadCode/data/bug-13307.php | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-13307.php diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index d83c65e4ee..e79a185935 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -361,6 +361,12 @@ public function testBug13311(): void $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; 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..3591312e57 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php @@ -0,0 +1,24 @@ + Date: Thu, 31 Jul 2025 13:03:26 +0200 Subject: [PATCH 16/20] Update bug-13307.php --- tests/PHPStan/Rules/DeadCode/data/bug-13307.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13307.php b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php index 3591312e57..78dc84dd4b 100644 --- a/tests/PHPStan/Rules/DeadCode/data/bug-13307.php +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13307.php @@ -10,14 +10,14 @@ class HelloWorld { public function testMethod(): string { - var_dump("dd"); + var_dump("\Bug13307\dd"); return "test"; } public function testMethod2(): string { - var_dump("DD"); + var_dump("\Bug13307\DD"); return "test"; } From cd46553d69d59a0e2f0c96e087f8b87ec2f300b0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 08:22:42 +0200 Subject: [PATCH 17/20] address feedback --- phpstan-baseline.neon | 2 +- src/Analyser/NodeScopeResolver.php | 17 +++-- src/Type/TypeUtils.php | 18 +++++ .../Rules/Pure/PureFunctionRuleTest.php | 12 ++++ .../PHPStan/Rules/Pure/data/pure-function.php | 71 ++++++++++++++++++- 5 files changed, 107 insertions(+), 13 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 47101ea9fe..8b9c61947a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1824,7 +1824,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' identifier: phpstanApi.instanceofType - count: 3 + count: 4 path: src/Type/TypeUtils.php - diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 06349dc059..667445d34d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5139,21 +5139,20 @@ private function processArgs( $scopeToPass = $closureBindScope; } - if ( - $parameterType === null - || $parameterType instanceof MixedType - || $parameterType->isCallable()->no() - ) { - $callCallbackImmediately = false; - } elseif ($parameter instanceof ExtendedParameterReflection) { + $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) { diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 7213a8140f..d31be8d72e 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 || $type instanceof IntersectionType) { + foreach ($type->getTypes() as $innerType) { + $callableType = self::findCallableType($innerType); + if ($callableType !== null) { + return $callableType; + } + } + } + + return null; + } + /** * @return HasPropertyType[] */ diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index dd2dfe3b96..08fc8581eb 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -116,6 +116,18 @@ public function testRule(): void 'Impure call to function array_push() in pure function PureFunction\bug13288().', 182, ], + [ + 'Impure exit in pure function PureFunction\bug13288b().', + 200, + ], + [ + 'Impure exit in pure function PureFunction\bug13288c().', + 217, + ], + [ + 'Impure exit in pure function PureFunction\bug13288d().', + 230, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-function.php b/tests/PHPStan/Rules/Pure/data/pure-function.php index 44901193d2..295e0c364d 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-function.php +++ b/tests/PHPStan/Rules/Pure/data/pure-function.php @@ -168,7 +168,7 @@ function justContainsInlineHtml() /** @phpstan-pure */ function bug13288(array $a) { - array_push($a, function() { // error because by ref arg + array_push($a, function () { // error because by ref arg exit(); // ok, as array_push() will not invoke the function }); @@ -176,11 +176,76 @@ function bug13288(array $a) fn() => exit() // ok, as array_push() will not invoke the function ); - $closure = function() { + $exitingClosure = function () { exit(); }; array_push($a, // error because by ref arg - $closure // ok, as array_push() will not invoke the function + $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 + } } From 588f78c818de16f1aa57ae04c4fc889ca8616bc1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 08:31:42 +0200 Subject: [PATCH 18/20] lazier --- src/Type/TypeUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index d31be8d72e..d649cfe0a7 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -176,7 +176,7 @@ public static function findCallableType(Type $type): ?Type return $type; } - if ($type instanceof UnionType || $type instanceof IntersectionType) { + if ($type instanceof UnionType) { foreach ($type->getTypes() as $innerType) { $callableType = self::findCallableType($innerType); if ($callableType !== null) { From 6414dc5a24e043b38876a326f98e4547d202809c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 08:35:08 +0200 Subject: [PATCH 19/20] Added regression test --- .../Rules/Pure/PureFunctionRuleTest.php | 5 +++++ tests/PHPStan/Rules/Pure/data/bug-12119.php | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/PHPStan/Rules/Pure/data/bug-12119.php diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 08fc8581eb..f0f39b2b5e 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -204,4 +204,9 @@ public function testBug13201(): void $this->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; + } +} From 255a863decad9584aabed2507a92c2f2dae3bf9e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 08:35:51 +0200 Subject: [PATCH 20/20] Update phpstan-baseline.neon --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8b9c61947a..47101ea9fe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1824,7 +1824,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\IntersectionType is error\-prone and deprecated\.$#' identifier: phpstanApi.instanceofType - count: 4 + count: 3 path: src/Type/TypeUtils.php -