diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 77f5969ae7..1f661f5a98 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -328,7 +328,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + 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..872fac5c79 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -429,7 +429,9 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e )); $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { - return $specifiedTypes; + return $specifiedTypes + ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->setRootExpr($specifiedTypes->getRootExpr()); } } } 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..06788c8941 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1593,6 +1593,15 @@ public function testBug9172(): void $this->assertNotEmpty($errors); } + #[RequiresPhp('>= 8.1.0')] + 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..d8004dff9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/pr-5880.php @@ -0,0 +1,37 @@ += 8.0 + +declare(strict_types = 1); + +namespace PR5880EndlessRecursion; + +class A { + public function a(): void {} +} +class B {} + +/** @template-covariant T of A|B = A|B */ +interface FooInterface +{ + /** + * @phpstan-assert-if-true static $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(); + } + } + +} 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); + } +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index caaaf53050..77e727ecb5 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -452,6 +452,31 @@ 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, + ], + [ + '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%.', + ], + ]); + } + #[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..4c4b007f28 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/self-contradiction.php @@ -0,0 +1,113 @@ +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; + } +} + +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) + */ + 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; + } +} + +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; + } +}