From 819afd2addd199b2bd94305db0cf9e3050aa7056 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 20 Jul 2025 18:35:11 +0200 Subject: [PATCH] Nothing is lower than Null --- src/Type/NullType.php | 8 ++++++ .../UndecidedComparisonCompoundTypeTrait.php | 8 ++++++ .../Traits/UndecidedComparisonTypeTrait.php | 15 ++++++++++- tests/PHPStan/Analyser/nsrt/bcmath-number.php | 8 +++--- .../Rules/Methods/CallMethodsRuleTest.php | 20 ++++++++++++++ .../PHPStan/Rules/Methods/data/bug-10719.php | 16 ++++++++++++ tests/PHPStan/Rules/Methods/data/bug-9141.php | 26 +++++++++++++++++++ 7 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-10719.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9141.php diff --git a/src/Type/NullType.php b/src/Type/NullType.php index bd52f45911..ef2408e713 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -110,6 +110,10 @@ public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryL return $otherType->isGreaterThan($this, $phpVersion); } + if ($otherType->isObject()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -123,6 +127,10 @@ public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): T return $otherType->isGreaterThanOrEqual($this, $phpVersion); } + if ($otherType->isObject()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } diff --git a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php index 6adf571d54..afec40a13e 100644 --- a/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonCompoundTypeTrait.php @@ -13,11 +13,19 @@ trait UndecidedComparisonCompoundTypeTrait public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes() && $this->isObject()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } diff --git a/src/Type/Traits/UndecidedComparisonTypeTrait.php b/src/Type/Traits/UndecidedComparisonTypeTrait.php index 6761274cf1..489aee2f6e 100644 --- a/src/Type/Traits/UndecidedComparisonTypeTrait.php +++ b/src/Type/Traits/UndecidedComparisonTypeTrait.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; trait UndecidedComparisonTypeTrait @@ -12,11 +13,19 @@ trait UndecidedComparisonTypeTrait public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { + if ($otherType->isNull()->yes() && $this->isObject()->yes()) { + return TrinaryLogic::createNo(); + } + return TrinaryLogic::createMaybe(); } @@ -32,11 +41,15 @@ public function getSmallerOrEqualType(PhpVersion $phpVersion): Type public function getGreaterType(PhpVersion $phpVersion): Type { - return new MixedType(); + return new MixedType(subtractedType: new NullType()); } public function getGreaterOrEqualType(PhpVersion $phpVersion): Type { + if ($this->isObject()->yes()) { + return new MixedType(subtractedType: new NullType()); + } + return new MixedType(); } diff --git a/tests/PHPStan/Analyser/nsrt/bcmath-number.php b/tests/PHPStan/Analyser/nsrt/bcmath-number.php index 2bdd9611b4..2ead45175f 100644 --- a/tests/PHPStan/Analyser/nsrt/bcmath-number.php +++ b/tests/PHPStan/Analyser/nsrt/bcmath-number.php @@ -190,10 +190,10 @@ public function bcVsNull(Number $a): void assertType('*ERROR*', $a ** $b); assertType('*ERROR*', $a << $b); assertType('*ERROR*', $a >> $b); - assertType('bool', $a < $b); - assertType('bool', $a <= $b); - assertType('bool', $a > $b); - assertType('bool', $a >= $b); + assertType('false', $a < $b); + assertType('false', $a <= $b); + assertType('true', $a > $b); + assertType('true', $a >= $b); assertType('int<-1, 1>', $a <=> $b); assertType('bool', $a == $b); assertType('bool', $a != $b); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8b55e66a03..d423d80dbb 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3588,6 +3588,26 @@ public function testBug13171(): void ]); } + public function testBug10719(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-10719.php'], []); + } + + public function testBug9141(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-9141.php'], []); + } + public function testBug3396(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-10719.php b/tests/PHPStan/Rules/Methods/data/bug-10719.php new file mode 100644 index 0000000000..abe94e40cf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10719.php @@ -0,0 +1,16 @@ +setTimestamp($dt2->getTimestamp() + 1000); +} + +if ($dt1 > $dt2) { + echo $dt1->getTimestamp(); +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9141.php b/tests/PHPStan/Rules/Methods/data/bug-9141.php new file mode 100644 index 0000000000..bfe85c0658 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9141.php @@ -0,0 +1,26 @@ +startTime = new DateTimeImmutable(); + } + + public function getStartTime(): ?DateTimeImmutable + { + return $this->startTime; + } + +} + +$helloWorld = new HelloWorld(); +if ($helloWorld->getStartTime() > new DateTimeImmutable()) { + echo sprintf('%s', $helloWorld->getStartTime()->format('d.m.y.')); +}