From 1d1b34664d37b9359eeac46df49863171d5fa514 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 11 Aug 2025 11:07:20 +0200 Subject: [PATCH 1/9] Fix false positive of function.alreadyNarrowedType (function call variable assignment) --- .../Comparison/ImpossibleCheckTypeHelper.php | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 0551a2b2d1..54570de4ae 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -68,11 +68,19 @@ public function findSpecifiedType( if ($node->isFirstClassCallable()) { return null; } - $argsCount = count($node->getArgs()); + $args = $node->getArgs(); + $argsCount = count($args); + + foreach ($args as $arg) { + if ($arg->value instanceof Expr\Assign) { + return null; + } + } + if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); if ($functionName === 'assert' && $argsCount >= 1) { - $arg = $node->getArgs()[0]->value; + $arg = $args[0]->value; $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); $assertValueIsTrue = $assertValue->isTrue()->yes(); if (! $assertValueIsTrue && ! $assertValue->isFalse()->yes()) { @@ -96,7 +104,7 @@ public function findSpecifiedType( } elseif ($functionName === 'array_search') { return null; } elseif ($functionName === 'in_array' && $argsCount >= 2) { - $haystackArg = $node->getArgs()[1]->value; + $haystackArg = $args[1]->value; $haystackType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($haystackArg) : $scope->getNativeType($haystackArg)); if ($haystackType instanceof MixedType) { return null; @@ -106,12 +114,12 @@ public function findSpecifiedType( return null; } - $needleArg = $node->getArgs()[0]->value; + $needleArg = $args[0]->value; $needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg)); $isStrictComparison = false; if ($argsCount >= 3) { - $strictNodeType = $scope->getType($node->getArgs()[2]->value); + $strictNodeType = $scope->getType($args[2]->value); $isStrictComparison = $strictNodeType->isTrue()->yes(); } @@ -192,7 +200,7 @@ public function findSpecifiedType( } } } elseif ($functionName === 'method_exists' && $argsCount >= 2) { - $objectArg = $node->getArgs()[0]->value; + $objectArg = $args[0]->value; $objectType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg)); if ($objectType instanceof ConstantStringType @@ -201,7 +209,7 @@ public function findSpecifiedType( return false; } - $methodArg = $node->getArgs()[1]->value; + $methodArg = $args[1]->value; $methodType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($methodArg) : $scope->getNativeType($methodArg)); if ($methodType instanceof ConstantStringType) { From 27b77695d84addeb1a5d4264b8b28801767b819e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 11 Aug 2025 11:08:06 +0200 Subject: [PATCH 2/9] Added regression test --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 6 ++++ .../Rules/Comparison/data/bug-13268.php | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13268.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 132ec6bfa2..9ab8ea3769 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1032,4 +1032,10 @@ public function testBug13291(): void $this->analyse([__DIR__ . '/data/bug-13291.php'], []); } + public function testBug13268(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13268.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13268.php b/tests/PHPStan/Rules/Comparison/data/bug-13268.php new file mode 100644 index 0000000000..0bdbd130eb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13268.php @@ -0,0 +1,28 @@ + Date: Tue, 12 Aug 2025 10:32:04 +0200 Subject: [PATCH 3/9] improve fix --- .../Comparison/ImpossibleCheckTypeHelper.php | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 54570de4ae..ab412e21ec 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -70,13 +70,6 @@ public function findSpecifiedType( } $args = $node->getArgs(); $argsCount = count($args); - - foreach ($args as $arg) { - if ($arg->value instanceof Expr\Assign) { - return null; - } - } - if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); if ($functionName === 'assert' && $argsCount >= 1) { @@ -286,6 +279,16 @@ public function findSpecifiedType( $results = []; + $assignedInCallVars = []; + if ($node instanceof Expr\CallLike) { + foreach ($node->getArgs() as $arg) { + if (!$arg->value instanceof Expr\Assign) { + continue; + } + + $assignedInCallVars[] = $arg->value; + } + } foreach ($sureTypes as $sureType) { if (self::isSpecified($typeSpecifierScope, $node, $sureType[0])) { $results[] = TrinaryLogic::createMaybe(); @@ -301,6 +304,14 @@ public function findSpecifiedType( /** @var Type $resultType */ $resultType = $sureType[1]; + foreach ($assignedInCallVars as $assignedInCallVar) { + if ($sureType[0] !== $assignedInCallVar->var) { + continue; + } + + $argumentType = $scope->getType($assignedInCallVar->expr); + } + $results[] = $resultType->isSuperTypeOf($argumentType)->result; } From 276e275851a6f233768d30cc338a7e8af137bd72 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Aug 2025 10:40:57 +0200 Subject: [PATCH 4/9] Added regression test --- .../ImpossibleCheckTypeFunctionCallRuleTest.php | 16 ++++++++++++++++ .../PHPStan/Rules/Comparison/data/bug-12087.php | 15 +++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12087.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 9ab8ea3769..91f6b583e7 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1032,10 +1032,26 @@ public function testBug13291(): void $this->analyse([__DIR__ . '/data/bug-13291.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug13268(): void { $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13268.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug12087(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12087.php'], [ + [ + 'Call to function is_null() with null will always evaluate to true.', + 14, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12087.php b/tests/PHPStan/Rules/Comparison/data/bug-12087.php new file mode 100644 index 0000000000..1ecb72ac6f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12087.php @@ -0,0 +1,15 @@ + Date: Tue, 12 Aug 2025 10:46:06 +0200 Subject: [PATCH 5/9] Added regression test --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 14 +++++++++++ .../Rules/Comparison/data/bug-9666.php | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-9666.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 91f6b583e7..5ac371307d 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1054,4 +1054,18 @@ public function testBug12087(): void ]); } + public function testBug9666(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9666.php'], [ + [ + 'Call to function is_bool() with bool will always evaluate to true.', + 20, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9666.php b/tests/PHPStan/Rules/Comparison/data/bug-9666.php new file mode 100644 index 0000000000..028601f5c2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9666.php @@ -0,0 +1,24 @@ + + */ + function b() + { + return []; + } +} + +function doFoo() { + $a = new A(); + $b = $a->b(); + $c = null; + if ($b && is_bool($c = reset($b))) { + // + } +} + From 5a5336c43d7294405433ded9652bad4377eaabf8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Aug 2025 10:49:18 +0200 Subject: [PATCH 6/9] Added regression test --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 12 ++++++ .../Rules/Comparison/data/bug-7773.php | 40 +++++++++++++++++++ .../Rules/Comparison/data/bug-9445.php | 20 ++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-7773.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-9445.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 5ac371307d..97566a0435 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1068,4 +1068,16 @@ public function testBug9666(): void ]); } + public function testBug9445(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9445.php'], []); + } + + public function testBug7773(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7773.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7773.php b/tests/PHPStan/Rules/Comparison/data/bug-7773.php new file mode 100644 index 0000000000..f7619b92a1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7773.php @@ -0,0 +1,40 @@ + $data json array + * @return string json string + * @throws JSONEncodingException + */ + public static function JSONEncode(array $data): string + { + if (!is_string($data = json_encode($data))) + throw new JSONEncodingException(); + return $data; + } + + /** + * Decodes the JSON data as an array + * @param string $data json string + * @return array json array + * @throws JSONDecodingException + */ + public static function JSONDecode(string $data): array + { + if (!is_array($data = json_decode($data, true))) + throw new JSONDecodingException(); + return $data; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9445.php b/tests/PHPStan/Rules/Comparison/data/bug-9445.php new file mode 100644 index 0000000000..b8693f6f8a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9445.php @@ -0,0 +1,20 @@ +id === $foo->id) { + return true; + } + } while (!is_null($foo = $foo->parent)); + + return false; + } +} From d30e95bf8f309184ef795886df54410d35a107d2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Aug 2025 10:53:08 +0200 Subject: [PATCH 7/9] fix php 8.x build --- .../Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php | 2 +- tests/PHPStan/Rules/Comparison/data/bug-12087.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 97566a0435..6dc1f3faf5 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1039,7 +1039,7 @@ public function testBug13268(): void $this->analyse([__DIR__ . '/data/bug-13268.php'], []); } - #[RequiresPhp('>= 8.0')] + #[RequiresPhp('>= 8.1')] public function testBug12087(): void { $tipText = '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/bug-12087.php b/tests/PHPStan/Rules/Comparison/data/bug-12087.php index 1ecb72ac6f..31b18128b1 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-12087.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-12087.php @@ -1,4 +1,4 @@ -= 8.1 namespace Bug12087; From df6de8d49d703c73243f23f6c57a726239dee714 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Aug 2025 11:02:19 +0200 Subject: [PATCH 8/9] Test MethodCall, StaticMethodCall --- .../ImpossibleCheckTypeMethodCallRuleTest.php | 15 ++++++++ ...sibleCheckTypeStaticMethodCallRuleTest.php | 16 ++++++++ .../Rules/Comparison/data/bug-12087b.php | 38 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12087b.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 3fd625a4b1..fb64511aff 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -275,6 +275,21 @@ public function testBug12473(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug12087b(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12087b.php'], [ + [ + 'Call to method Bug12087b\MyAssert::is_null() with null will always evaluate to true.', + 37, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 1b43cdea9d..8a68085379 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -145,6 +146,21 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast $this->analyse([__DIR__ . '/data/impossible-static-method-report-always-true-last-condition.php'], $expectedErrors); } + #[RequiresPhp('>= 8.1')] + public function testBug12087b(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12087b.php'], [ + [ + 'Call to static method Bug12087b\MyAssert::static_is_null() with null will always evaluate to true.', + 31, + $tipText, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12087b.php b/tests/PHPStan/Rules/Comparison/data/bug-12087b.php new file mode 100644 index 0000000000..3c26c567e9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12087b.php @@ -0,0 +1,38 @@ += 8.1 + +namespace Bug12087b; + +enum Button: int +{ + case On = 1; + + case Off = 0; +} + +class MyAssert { + /** + * @return ($value is null ? true : false) + */ + static public function static_is_null(mixed $value): bool { + return $value === null; + } + + /** + * @return ($value is null ? true : false) + */ + public function is_null(mixed $value): bool { + return $value === null; + } +} + +function doFoo(): void { + $value = 10; + + MyAssert::static_is_null($value = Button::tryFrom($value)); +} + +function doBar(MyAssert $assert): void { + $value = 10; + + $assert->is_null($value = Button::tryFrom($value)); +} From acf93afeb892caf5f7b5f03728e0cbadf878a411 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Aug 2025 11:12:06 +0200 Subject: [PATCH 9/9] fix php 7.x build --- .../Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php | 1 + tests/PHPStan/Rules/Comparison/data/bug-9445.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 6dc1f3faf5..2c0c7fbbfe 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1068,6 +1068,7 @@ public function testBug9666(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug9445(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9445.php b/tests/PHPStan/Rules/Comparison/data/bug-9445.php index b8693f6f8a..8fd828d0cc 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-9445.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-9445.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types=1); namespace Bug9445;