From 7438b86c1b22c81956e76c4b326cbf5c64cadc14 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 21 Feb 2022 21:09:49 +0100 Subject: [PATCH] Improve intersect and union of SubtractableTypes Converts non-SubtractableTypes to SubtractableTypes in union to not loose subtracted type information. Sorts the SubtractableTypes in intersect to not loose subtracted type information if SubtractableTypes are intersected where not all of them have subtracted types configured. --- src/Type/TypeCombinator.php | 18 ++++++++++-- .../Analyser/LegacyNodeScopeResolverTest.php | 2 +- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-6672.php | 28 +++++++++++++++++++ tests/PHPStan/Analyser/data/instanceof.php | 2 +- .../data/root-scope-maybe-defined.php | 2 +- tests/PHPStan/Type/TypeCombinatorTest.php | 16 +++++++++++ 7 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-6672.php diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 0e11e8da8b6..73ab5598da1 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 bee0a4cb33b..cf48e6ec937 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 0795d0586b9..3316b946b97 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 00000000000..ed7a0364913 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6672.php @@ -0,0 +1,28 @@ + 17) { + assertType('int<18, max>', $a); + } + + if ($b > 17 || $b === null) { + assertType('int<18, max>|null', $b); + } + + if ($c < 17) { + assertType('int', $c); + } + + if ($d < 17 || $d === null) { + assertType('int|null', $d); + } + + if ($e >= 17 && $e <= 19 || $e === null) { + assertType('int<17, 19>|null', $e); + } +} diff --git a/tests/PHPStan/Analyser/data/instanceof.php b/tests/PHPStan/Analyser/data/instanceof.php index 098b74cb47b..bf1d12d2d2f 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 b8fa432f9b0..800858e8278 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 992e855fbde..c2f05851c9b 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', + ]; } /**