diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 0e11e8da8b..73ab5598da 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -356,9 +356,10 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b); } if ($isSuperType->yes()) { - $subtractedType = null; if ($b instanceof SubtractableType) { $subtractedType = $b->getSubtractedType(); + } else { + $subtractedType = new MixedType(false, $b); } $a = self::intersectWithSubtractedType($a, $subtractedType); return [$a, null]; @@ -373,9 +374,10 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a); } if ($isSuperType->yes()) { - $subtractedType = null; if ($a instanceof SubtractableType) { $subtractedType = $a->getSubtractedType(); + } else { + $subtractedType = new MixedType(false, $a); } $b = self::intersectWithSubtractedType($b, $subtractedType); return [null, $b]; @@ -711,6 +713,18 @@ public static function intersect(Type ...$types): Type $typesCount = count($types); } + // move subtractables with subtracts before those without to avoid loosing them in the union logic + usort($types, static function (Type $a, Type $b): int { + if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { + return -1; + } + if ($b instanceof SubtractableType && $b->getSubtractedType() !== null) { + return 1; + } + + return 0; + }); + // transform IntegerType & ConstantIntegerType to ConstantIntegerType // transform Child & Parent to Child // transform Object & ~null to Object diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index bee0a4cb33..cf48e6ec93 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -697,7 +697,7 @@ public function dataAssignInIf(): array $testScope, 'mixed', TrinaryLogic::createYes(), - 'mixed', // should be mixed~bool+1 + 'mixed~bool', ], [ $testScope, diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0795d0586b..3316b946b9 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -706,6 +706,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6488.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6624.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6672.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-6672.php b/tests/PHPStan/Analyser/data/bug-6672.php new file mode 100644 index 0000000000..1ae5b9c0f7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6672.php @@ -0,0 +1,44 @@ + 17) { + assertType('int<18, max>', $a); + } else { + assertType('int', $a); + } + + if ($b > 17 || $b === null) { + assertType('int<18, max>|null', $b); + } else { + assertType('int', $b); + } + + if ($c < 17) { + assertType('int', $c); + } else { + assertType('int<17, max>', $c); + } + + if ($d < 17 || $d === null) { + assertType('int|null', $d); + } else { + assertType('int<17, max>', $d); + } + + if ($e >= 17 && $e <= 19 || $e === null) { + assertType('int<17, 19>|null', $e); + } else { + assertType('int|int<20, max>', $e); + } + + if ($f < 17 || $f > 19 || $f === null) { + assertType('int|int<20, max>|null', $f); + } else { + assertType('int<17, 19>', $f); + } +} diff --git a/tests/PHPStan/Analyser/data/instanceof.php b/tests/PHPStan/Analyser/data/instanceof.php index 098b74cb47..bf1d12d2d2 100644 --- a/tests/PHPStan/Analyser/data/instanceof.php +++ b/tests/PHPStan/Analyser/data/instanceof.php @@ -156,7 +156,7 @@ public function testExprInstanceof($subject, string $classString, $union, $inter assertType('object', $subject); assertType('bool', $subject instanceof $string); } else { - assertType('mixed', $subject); + assertType('mixed~MixedT (method InstanceOfNamespace\Foo::testExprInstanceof(), argument)', $subject); assertType('bool', $subject instanceof $string); } diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php index b8fa432f9b..800858e827 100644 --- a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php +++ b/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php @@ -16,7 +16,7 @@ \PHPStan\Testing\assertType('1', $baz); } -\PHPStan\Testing\assertType('mixed', $baz); +\PHPStan\Testing\assertType('mixed~null', $baz); function () { \PHPStan\Testing\assertVariableCertainty(TrinaryLogic::createNo(), $foo); diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 992e855fbd..c2f05851c9 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2074,6 +2074,14 @@ public function dataUnion(): iterable UnionType::class, 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum::ONE', ]; + yield [ + [ + new MixedType(false, new IntegerRangeType(17, null)), + new IntegerRangeType(19, null), + ], + MixedType::class, + 'mixed~int<17, 18>=implicit', + ]; } /** @@ -3395,6 +3403,14 @@ public function dataIntersect(): iterable NeverType::class, '*NEVER*', ]; + yield [ + [ + new MixedType(false, new IntegerRangeType(17, null)), + new MixedType(), + ], + MixedType::class, + 'mixed~int<17, max>=implicit', + ]; } /**