From 03846741fd4e2ab8be7df1003c38b81e94de1496 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:43:08 +0000 Subject: [PATCH 01/28] Remember call expression as truthy/falsey alongside `@phpstan-assert-if-true` argument narrowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When a function/method/static call carries `@phpstan-assert*` assertions, the call handlers previously returned only the assert-specified argument narrowing and skipped `handleDefaultTruthyOrFalseyContext`, so the call expression itself was not remembered as truthy/falsey in the branch. Re-evaluating it (e.g. `if (is_readable($p)) { require $p; }`) then yielded `bool` instead of `true`. - `FuncCallHandler`, `MethodCallHandler` and `StaticCallHandler` now union the asserts result with the default truthy/falsey narrowing, while preserving the asserts' original root expression so impossible-check detection is unaffected. - `ImpossibleCheckTypeHelper` now ignores the self-referential narrowing entry (the checked call expression itself), which carries no information about whether the check is redundant — the informative narrowing lives in the argument entries. - `RequireFileExistsRule` additionally treats `is_readable`, `is_writable`, `is_writeable` and `is_executable` (next to `file_exists`/`is_file`) as guards that prove the path exists, via a named `FILE_EXISTENCE_FUNCTIONS` constant. - Fixed a genuine `X && !X` bug in the build-only `OrChainIdenticalComparisonToInArrayRule::getSubjectAndValue()` that the improved narrowing newly detects. --- src/Analyser/ExprHandler/FuncCallHandler.php | 4 +++ .../ExprHandler/MethodCallHandler.php | 8 ++++- .../ExprHandler/StaticCallHandler.php | 8 ++++- tests/PHPStan/Analyser/nsrt/bug-14829.php | 33 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index fa39b3cd59..d14a2d830b 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -936,6 +936,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { + // Asserts narrow the arguments, but the call expression itself + // must still be remembered as truthy/falsey so that re-evaluating + // it in the same branch (e.g. `if (is_readable($p)) require $p;`) + // keeps the narrowed result. Keep the asserts' root expression. return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 77f5969ae7..87cdb6a390 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -328,7 +328,13 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + // Asserts narrow the arguments, but the call expression itself + // must still be remembered as truthy/falsey so that re-evaluating + // it in the same branch keeps the narrowed result. Keep the + // asserts' root expression. + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index e24683ac8f..a31b79986d 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -429,7 +429,13 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + // Asserts narrow the arguments, but the call expression itself + // must still be remembered as truthy/falsey so that re-evaluating + // it in the same branch keeps the narrowed result. Keep the + // asserts' root expression. + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14829.php b/tests/PHPStan/Analyser/nsrt/bug-14829.php index 381a638272..99fcc8c4f4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14829.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14829.php @@ -4,6 +4,23 @@ use function PHPStan\Testing\assertType; +class Checker +{ + + /** @phpstan-assert-if-true =non-empty-string $path */ + public function isReadable(string $path): bool + { + return $path !== ''; + } + + /** @phpstan-assert-if-true =non-empty-string $path */ + public static function staticIsReadable(string $path): bool + { + return $path !== ''; + } + +} + function testFunction(string $path): void { // is_readable() has @phpstan-assert-if-true in stubs/file.stub @@ -17,3 +34,19 @@ function testFunction(string $path): void } assertType('true', is_readable($path)); } + +function testMethod(Checker $c, string $path): void +{ + if ($c->isReadable($path)) { + assertType('true', $c->isReadable($path)); + assertType('non-empty-string', $path); + } +} + +function testStaticMethod(string $path): void +{ + if (Checker::staticIsReadable($path)) { + assertType('true', Checker::staticIsReadable($path)); + assertType('non-empty-string', $path); + } +} From c056e6ac40c7293af77a84cd6f31345b3e37af06 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jun 2026 17:19:40 +0200 Subject: [PATCH 02/28] remove comments --- src/Analyser/ExprHandler/FuncCallHandler.php | 4 ---- src/Analyser/ExprHandler/MethodCallHandler.php | 4 ---- src/Analyser/ExprHandler/StaticCallHandler.php | 4 ---- 3 files changed, 12 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index d14a2d830b..fa39b3cd59 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -936,10 +936,6 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - // Asserts narrow the arguments, but the call expression itself - // must still be remembered as truthy/falsey so that re-evaluating - // it in the same branch (e.g. `if (is_readable($p)) require $p;`) - // keeps the narrowed result. Keep the asserts' root expression. return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 87cdb6a390..1f661f5a98 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -328,10 +328,6 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - // Asserts narrow the arguments, but the call expression itself - // must still be remembered as truthy/falsey so that re-evaluating - // it in the same branch keeps the narrowed result. Keep the - // asserts' root expression. return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index a31b79986d..872fac5c79 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -429,10 +429,6 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - // Asserts narrow the arguments, but the call expression itself - // must still be remembered as truthy/falsey so that re-evaluating - // it in the same branch keeps the narrowed result. Keep the - // asserts' root expression. return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); From 03e2c1f867e503e0463f90e0676bbafd611fbf9e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jun 2026 17:26:02 +0200 Subject: [PATCH 03/28] added regression test --- .../BooleanAndConstantConditionRuleTest.php | 11 ++++++ .../Comparison/data/self-contradiction.php | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/self-contradiction.php diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index caaaf53050..e8ee2c2f85 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -452,6 +452,17 @@ public function testBug8555(): void $this->analyse([__DIR__ . '/data/bug-8555.php'], []); } + public function testSelfContradiction(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/self-contradiction.php'], [ + [ + 'Result of && is always false.', + 25 + ] + ]); + } + #[RequiresPhp('>= 8.1.0')] public function testBug14807(): void { diff --git a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php new file mode 100644 index 0000000000..47a56712ab --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php @@ -0,0 +1,35 @@ +left) && !self::isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } +} From 51c1efc13f066523498ed0a3c42af0521f2c3411 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 15 Jun 2026 18:32:32 +0200 Subject: [PATCH 04/28] Update BooleanAndConstantConditionRuleTest.php --- .../Rules/Comparison/BooleanAndConstantConditionRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index e8ee2c2f85..6514f8619f 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -458,8 +458,8 @@ public function testSelfContradiction(): void $this->analyse([__DIR__ . '/data/self-contradiction.php'], [ [ 'Result of && is always false.', - 25 - ] + 25, + ], ]); } From 4efd0c9f18de948bbbafa7a1185d3e97b0c4c994 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 06:17:14 +0200 Subject: [PATCH 05/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index b229137309..a35f300766 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -236,7 +236,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ try { $json = Json::decode($stdout, Json::FORCE_ARRAY); } catch (Throwable $e) { - echo $stdout . "\n"; + $output->writeln($stdout); throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); } From c18f8edd687f520510241c22410c78593c82ddba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 06:24:31 +0200 Subject: [PATCH 06/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index a35f300766..559639159c 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -223,6 +223,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'analyse', '--error-format', 'json', + '--debug', '--no-progress', '-c', escapeshellarg($neonPath), From f880943f2299419aa15f62320c233268c8ab1ac0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 08:49:31 +0200 Subject: [PATCH 07/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 559639159c..a5acec797f 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -223,7 +223,6 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'analyse', '--error-format', 'json', - '--debug', '--no-progress', '-c', escapeshellarg($neonPath), @@ -237,7 +236,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ try { $json = Json::decode($stdout, Json::FORCE_ARRAY); } catch (Throwable $e) { - $output->writeln($stdout); + $output->writeln(sprintf('Error in analysis of %s: %s', $hash, $stdout)); throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); } From 62b81480e242a5703dcf0ba6f7ed3140ff113bca Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 08:57:37 +0200 Subject: [PATCH 08/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index a5acec797f..dfa81d903b 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -236,7 +236,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ try { $json = Json::decode($stdout, Json::FORCE_ARRAY); } catch (Throwable $e) { - $output->writeln(sprintf('Error in analysis of %s: %s', $hash, $stdout)); + printf("Error in analysis of %s: %s\n", $hash, $stdout); throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); } From b0e8bb0fd437cd0943e5c97591d061585d3beee8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 09:08:47 +0200 Subject: [PATCH 09/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 43 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index dfa81d903b..8fbc1ebe52 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -232,30 +232,35 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ $output->writeln(sprintf('Starting analysis of %s', $hash)); $process = new ProcessPromise($loop, implode(' ', $commandArray)); $startTime = microtime(true); - return $process->run()->then(static function (string $stdout) use ($hash, $output, $startTime) { - try { - $json = Json::decode($stdout, Json::FORCE_ARRAY); - } catch (Throwable $e) { - printf("Error in analysis of %s: %s\n", $hash, $stdout); - throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); - } + return $process->run()->then( + static function (string $stdout) use ($hash, $output, $startTime) { + try { + $json = Json::decode($stdout, Json::FORCE_ARRAY); + } catch (Throwable $e) { + printf("Error in analysis of %s: %s\n", $hash, $stdout); + throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); + } - $errors = []; - foreach ($json['files'] as ['messages' => $messages]) { - foreach ($messages as $message) { - $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); - if (strpos($messageText, 'Internal error') !== false) { - throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); + $errors = []; + foreach ($json['files'] as ['messages' => $messages]) { + foreach ($messages as $message) { + $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); + if (strpos($messageText, 'Internal error') !== false) { + throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); + } + $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); } - $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); } - } - $elapsedTime = microtime(true) - $startTime; - $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); + $elapsedTime = microtime(true) - $startTime; + $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); - return $errors; - }); + return $errors; + }, + static function (Throwable $throwable) use ($hash, $output) { + $output->writeln(sprintf('Analysis of %s errored %.2f s', $hash, $throwable->getMessage())); + } + ); } private function loadPlaygroundCache(): PlaygroundCache From 6c7628af52d4e433ee539f3749882b5d5accf574 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 09:08:59 +0200 Subject: [PATCH 10/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 8fbc1ebe52..642809fcf8 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -258,7 +258,7 @@ static function (string $stdout) use ($hash, $output, $startTime) { return $errors; }, static function (Throwable $throwable) use ($hash, $output) { - $output->writeln(sprintf('Analysis of %s errored %.2f s', $hash, $throwable->getMessage())); + $output->writeln(sprintf('Analysis of %s errored %s', $hash, $throwable->getMessage())); } ); } From b82421b060964fe2a128de00f0ec377ec20fda3d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 09:30:42 +0200 Subject: [PATCH 11/28] Update AnalyseCommand.php --- src/Command/AnalyseCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index d2e39d3300..8c273caf5a 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -500,6 +500,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); + $errorOutput->writeLineFormatted('internal errors '. implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors))); $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); $errorOutput->writeLineFormatted(' to get all reported errors.'); From 064d4ef28c8e83f94008f1644df06dc4b6e81a81 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 09:35:47 +0200 Subject: [PATCH 12/28] more debug --- src/Command/AnalyseCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 8c273caf5a..9a8eec2d04 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -481,6 +481,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorFormatter = $container->getService($errorFormatterServiceName); if (count($internalErrorsTuples) > 0) { + foreach ($internalErrorsTuples as [$internalError]) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + } + $analysisResult = new AnalysisResult( array_values($internalFileSpecificErrors), array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors), From d69a2a1d5993c86f11e4996eb92a52c8a1e030fb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 12:08:49 +0200 Subject: [PATCH 13/28] debug --- .github/workflows/issue-bot.yml | 2 +- issue-bot/console.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index dfa2ccbc71..de530abe86 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -139,7 +139,7 @@ jobs: - name: "Run PHPStan" working-directory: "issue-bot" - timeout-minutes: 5 + timeout-minutes: 10 run: ./console.php run ${{ steps.chunk.outputs.phpVersion }} ${{ steps.chunk.outputs.playgroundExamples }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/issue-bot/console.php b/issue-bot/console.php index fd1dfa73a8..0245bbde5e 100755 --- a/issue-bot/console.php +++ b/issue-bot/console.php @@ -28,6 +28,10 @@ use function implode; (static function (): void { + set_exception_handler(static function (\Throwable $e): void { + fwrite(STDERR, 'Swallowed by global exception handler: ' . $e->getMessage() . "\n"); + }); + $token = $_SERVER['GITHUB_PAT'] ?? 'unknown'; $phpstanSrcCommitBefore = $_SERVER['PHPSTAN_SRC_COMMIT_BEFORE'] ?? 'unknown'; From 65d8b6eaf69665513f052eed4787c3cd12ce7036 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 12:13:01 +0200 Subject: [PATCH 14/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 43 ++++++++++++---------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 642809fcf8..b229137309 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -232,35 +232,30 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ $output->writeln(sprintf('Starting analysis of %s', $hash)); $process = new ProcessPromise($loop, implode(' ', $commandArray)); $startTime = microtime(true); - return $process->run()->then( - static function (string $stdout) use ($hash, $output, $startTime) { - try { - $json = Json::decode($stdout, Json::FORCE_ARRAY); - } catch (Throwable $e) { - printf("Error in analysis of %s: %s\n", $hash, $stdout); - throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); - } + return $process->run()->then(static function (string $stdout) use ($hash, $output, $startTime) { + try { + $json = Json::decode($stdout, Json::FORCE_ARRAY); + } catch (Throwable $e) { + echo $stdout . "\n"; + throw new Exception(sprintf('Failed to decode JSON for %s: %s', $hash, $e->getMessage())); + } - $errors = []; - foreach ($json['files'] as ['messages' => $messages]) { - foreach ($messages as $message) { - $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); - if (strpos($messageText, 'Internal error') !== false) { - throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); - } - $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); + $errors = []; + foreach ($json['files'] as ['messages' => $messages]) { + foreach ($messages as $message) { + $messageText = str_replace(sprintf('/%s.php', $hash), '/tmp.php', $message['message']); + if (strpos($messageText, 'Internal error') !== false) { + throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); } + $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); } + } - $elapsedTime = microtime(true) - $startTime; - $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); + $elapsedTime = microtime(true) - $startTime; + $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); - return $errors; - }, - static function (Throwable $throwable) use ($hash, $output) { - $output->writeln(sprintf('Analysis of %s errored %s', $hash, $throwable->getMessage())); - } - ); + return $errors; + }); } private function loadPlaygroundCache(): PlaygroundCache From 2769b7883e5c388d9e19fecbad91fba786ca3870 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 12:25:05 +0200 Subject: [PATCH 15/28] debug --- issue-bot/console.php | 4 ---- issue-bot/src/Console/RunCommand.php | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/issue-bot/console.php b/issue-bot/console.php index 0245bbde5e..fd1dfa73a8 100755 --- a/issue-bot/console.php +++ b/issue-bot/console.php @@ -28,10 +28,6 @@ use function implode; (static function (): void { - set_exception_handler(static function (\Throwable $e): void { - fwrite(STDERR, 'Swallowed by global exception handler: ' . $e->getMessage() . "\n"); - }); - $token = $_SERVER['GITHUB_PAT'] ?? 'unknown'; $phpstanSrcCommitBefore = $_SERVER['PHPSTAN_SRC_COMMIT_BEFORE'] ?? 'unknown'; diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index b229137309..4346d04811 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -57,6 +57,10 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + set_exception_handler(static function (\Throwable $e): void { + fwrite(STDERR, 'Swallowed by global exception handler: ' . $e->getMessage() . "\n"); + }); + $phpVersion = (int) $input->getArgument('phpVersion'); $commaSeparatedPlaygroundHashes = $input->getArgument('playgroundHashes'); $playgroundHashes = explode(',', $commaSeparatedPlaygroundHashes); From 0581ca564ae55cba9c55391a6d535aaff178afde Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 14:17:53 +0200 Subject: [PATCH 16/28] test with reduced shard size --- issue-bot/src/Console/DownloadCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index d19b0fa9e6..0d5055d9b0 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($phpVersionHashes) === 0) { continue; } - $chunkSize = (int) ceil(count($phpVersionHashes) / 25); + $chunkSize = (int) ceil(count($phpVersionHashes) / 20); if ($chunkSize < 1) { throw new Exception('Chunk size less than 1'); } From 692690f078d9eb82ac63462165936a1ab2e39cd6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 14:19:38 +0200 Subject: [PATCH 17/28] Update AnalyseCommand.php --- src/Command/AnalyseCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 9a8eec2d04..4a71adbd9b 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -482,8 +482,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($internalErrorsTuples) > 0) { foreach ($internalErrorsTuples as [$internalError]) { - $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); - $inceptionResult->getStdOutput()->writeLineFormatted(''); + $errorOutput->writeLineFormatted($internalError->getMessage()); + $errorOutput->writeLineFormatted(''); } $analysisResult = new AnalysisResult( From 2a3d26eef729430caadefd4526dc5c7051df1384 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 14:31:56 +0200 Subject: [PATCH 18/28] smaler shards --- issue-bot/src/Console/DownloadCommand.php | 2 +- issue-bot/src/Console/RunCommand.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index 0d5055d9b0..1e8e25d0a0 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($phpVersionHashes) === 0) { continue; } - $chunkSize = (int) ceil(count($phpVersionHashes) / 20); + $chunkSize = (int) ceil(count($phpVersionHashes) / 10); if ($chunkSize < 1) { throw new Exception('Chunk size less than 1'); } diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 4346d04811..f64b48b783 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -227,6 +227,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'analyse', '--error-format', 'json', + '-vvv', '--no-progress', '-c', escapeshellarg($neonPath), From 8c9a00c9a183e3d56884446aaab9cb7566f8bd14 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 14:52:19 +0200 Subject: [PATCH 19/28] Update DownloadCommand.php --- issue-bot/src/Console/DownloadCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index 1e8e25d0a0..d95869dcb5 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($phpVersionHashes) === 0) { continue; } - $chunkSize = (int) ceil(count($phpVersionHashes) / 10); + $chunkSize = (int) ceil(count($phpVersionHashes) / 50); if ($chunkSize < 1) { throw new Exception('Chunk size less than 1'); } From 59da5742a334e1c85599a63e21197b3d50c4d12f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 15:14:50 +0200 Subject: [PATCH 20/28] Update DownloadCommand.php --- issue-bot/src/Console/DownloadCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index d95869dcb5..dabbd225a4 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($phpVersionHashes) === 0) { continue; } - $chunkSize = (int) ceil(count($phpVersionHashes) / 50); + $chunkSize = (int) ceil(count($phpVersionHashes) / 35); if ($chunkSize < 1) { throw new Exception('Chunk size less than 1'); } From de8e5d130cd817816a3083283f67a454398507fe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 15:21:52 +0200 Subject: [PATCH 21/28] cleanup --- .github/workflows/issue-bot.yml | 2 +- issue-bot/src/Console/DownloadCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index de530abe86..dfa2ccbc71 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -139,7 +139,7 @@ jobs: - name: "Run PHPStan" working-directory: "issue-bot" - timeout-minutes: 10 + timeout-minutes: 5 run: ./console.php run ${{ steps.chunk.outputs.phpVersion }} ${{ steps.chunk.outputs.playgroundExamples }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index dabbd225a4..d19b0fa9e6 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($phpVersionHashes) === 0) { continue; } - $chunkSize = (int) ceil(count($phpVersionHashes) / 35); + $chunkSize = (int) ceil(count($phpVersionHashes) / 25); if ($chunkSize < 1) { throw new Exception('Chunk size less than 1'); } From 0e0afccb3356a553a91a3dd1e7a2411381ad5e42 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 15:29:09 +0200 Subject: [PATCH 22/28] test with conditional return type --- .../BooleanAndConstantConditionRuleTest.php | 5 ++++ .../Comparison/data/self-contradiction.php | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 6514f8619f..ba2c6a2aa1 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -460,6 +460,11 @@ public function testSelfContradiction(): void 'Result of && is always false.', 25, ], + [ + 'Result of && is always false.', + 51, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.' + ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php index 47a56712ab..c985d75761 100644 --- a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php +++ b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php @@ -33,3 +33,29 @@ private function getSubjectAndValue(Identical $comparison): ?array return null; } } + +class Bar { + /** + * @return ($node is Scalar|ClassConstFetch|ConstFetch ? true : false) + */ + private static function isSubjectNode(Expr $node): bool + { + return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch; + } + + /** + * @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null + */ + private function getSubjectAndValue(Identical $comparison): ?array + { + if (self::isSubjectNode($comparison->left) && !self::isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } +} From 2e61effdc674cb9e0cecafeaf8e4cd40221cd940 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:08:21 +0200 Subject: [PATCH 23/28] Update RunCommand.php --- issue-bot/src/Console/RunCommand.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index f64b48b783..b229137309 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -57,10 +57,6 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - set_exception_handler(static function (\Throwable $e): void { - fwrite(STDERR, 'Swallowed by global exception handler: ' . $e->getMessage() . "\n"); - }); - $phpVersion = (int) $input->getArgument('phpVersion'); $commaSeparatedPlaygroundHashes = $input->getArgument('playgroundHashes'); $playgroundHashes = explode(',', $commaSeparatedPlaygroundHashes); @@ -227,7 +223,6 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'analyse', '--error-format', 'json', - '-vvv', '--no-progress', '-c', escapeshellarg($neonPath), From eb58ac4bf5f6ea41b00ce2e95ffe37c3d0ecc81a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:09:07 +0200 Subject: [PATCH 24/28] Update AnalyseCommand.php --- src/Command/AnalyseCommand.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 4a71adbd9b..d2e39d3300 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -481,11 +481,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorFormatter = $container->getService($errorFormatterServiceName); if (count($internalErrorsTuples) > 0) { - foreach ($internalErrorsTuples as [$internalError]) { - $errorOutput->writeLineFormatted($internalError->getMessage()); - $errorOutput->writeLineFormatted(''); - } - $analysisResult = new AnalysisResult( array_values($internalFileSpecificErrors), array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors), @@ -505,7 +500,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); - $errorOutput->writeLineFormatted('internal errors '. implode("\n", array_map(static fn (InternalError $internalError) => $internalError->getMessage(), $internalErrors))); $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); $errorOutput->writeLineFormatted(' to get all reported errors.'); From 71f215ba1bfdcbd6bf79b9c2ffa0eac75c3a3b14 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:17:32 +0200 Subject: [PATCH 25/28] Update BooleanAndConstantConditionRuleTest.php --- .../Rules/Comparison/BooleanAndConstantConditionRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index ba2c6a2aa1..0c58c8266d 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -463,7 +463,7 @@ public function testSelfContradiction(): void [ 'Result of && is always false.', 51, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.' + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); } From 674b39a71e1a8be282dae6d97ee8fd49355415ea Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:42:55 +0200 Subject: [PATCH 26/28] fix endless recursion --- src/Type/StaticType.php | 2 +- .../Analyser/AnalyserIntegrationTest.php | 8 +++++ tests/PHPStan/Analyser/data/pr-5880.php | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/data/pr-5880.php diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 2af58ab2e8..4b5abe1f7e 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -380,7 +380,7 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop } if (!$isFinal || $type instanceof ThisType) { - return $traverse($type); + return RecursionGuard::run($type, static fn () => $traverse($type)); } return $traverse($type->getStaticObjectType()); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index d6e9e7e38c..c42b3aabb0 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1593,6 +1593,14 @@ public function testBug9172(): void $this->assertNotEmpty($errors); } + public function testPr5880(): void + { + // endless loop + $errors = $this->runAnalyse(__DIR__ . '/data/pr-5880.php'); + $this->assertCount(1, $errors); + $this->assertSame('Call to an undefined method (T of PR5880EndlessRecursion\A = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B)|T of PR5880EndlessRecursion\B = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B::a().', $errors[0]->getMessage()); + } + public function testBug14707(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-14707.php'); diff --git a/tests/PHPStan/Analyser/data/pr-5880.php b/tests/PHPStan/Analyser/data/pr-5880.php new file mode 100644 index 0000000000..b1bda48b9d --- /dev/null +++ b/tests/PHPStan/Analyser/data/pr-5880.php @@ -0,0 +1,35 @@ + $this + */ + public function isA(): bool; + + /** @return T */ + public function get(): A|B; +} + +/** + * @template-covariant T of A|B = A|B + * @implements FooInterface + */ +abstract class Foo implements FooInterface +{ + public function other(): void + { + if ($this->isA()) { + $this->get()->a(); + } + } + +} From 92c3b5f844813c52ea674cd451589c26624992d0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:45:53 +0200 Subject: [PATCH 27/28] Update pr-5880.php --- tests/PHPStan/Analyser/data/pr-5880.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/data/pr-5880.php b/tests/PHPStan/Analyser/data/pr-5880.php index b1bda48b9d..d8004dff9a 100644 --- a/tests/PHPStan/Analyser/data/pr-5880.php +++ b/tests/PHPStan/Analyser/data/pr-5880.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace PR5880EndlessRecursion; From 77b85528e5bdebf3d955ed25d47b78a727b034ca Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 16 Jun 2026 21:53:03 +0200 Subject: [PATCH 28/28] more tests --- .../Analyser/AnalyserIntegrationTest.php | 1 + .../BooleanAndConstantConditionRuleTest.php | 9 +++ .../Comparison/data/self-contradiction.php | 56 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index c42b3aabb0..06788c8941 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1593,6 +1593,7 @@ public function testBug9172(): void $this->assertNotEmpty($errors); } + #[RequiresPhp('>= 8.1.0')] public function testPr5880(): void { // endless loop diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 0c58c8266d..77e727ecb5 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -463,6 +463,15 @@ public function testSelfContradiction(): void [ 'Result of && is always false.', 51, + ], + [ + 'Result of && is always false.', + 77, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Result of && is always false.', + 103, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); diff --git a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php index c985d75761..4c4b007f28 100644 --- a/tests/PHPStan/Rules/Comparison/data/self-contradiction.php +++ b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php @@ -8,7 +8,7 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Scalar; -class Foo { +class AssertStaticCall { /** * @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node */ @@ -34,7 +34,33 @@ private function getSubjectAndValue(Identical $comparison): ?array } } -class Bar { +class AssertMethodCall { + /** + * @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node + */ + private function isSubjectNode(Expr $node): bool + { + return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch; + } + + /** + * @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null + */ + private function getSubjectAndValue(Identical $comparison): ?array + { + if ($this->isSubjectNode($comparison->left) && !$this->isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!$this->isSubjectNode($comparison->left) && $this->isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } +} + +class AssertStaticConditionalReturn { /** * @return ($node is Scalar|ClassConstFetch|ConstFetch ? true : false) */ @@ -59,3 +85,29 @@ private function getSubjectAndValue(Identical $comparison): ?array return null; } } + +class AssertInstanceConditionalReturn { + /** + * @return ($node is Scalar|ClassConstFetch|ConstFetch ? true : false) + */ + private function isSubjectNode(Expr $node): bool + { + return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch; + } + + /** + * @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null + */ + private function getSubjectAndValue(Identical $comparison): ?array + { + if ($this->isSubjectNode($comparison->left) && !$this->isSubjectNode($comparison->left)) { + return ['subject' => $comparison->right, 'value' => $comparison->left]; + } + + if (!$this->isSubjectNode($comparison->left) && $this->isSubjectNode($comparison->right)) { + return ['subject' => $comparison->left, 'value' => $comparison->right]; + } + + return null; + } +}