From 522af3e2a41dfc79ae62d7ffbed348666a03f89b Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 21 Feb 2022 20:46:21 +0100 Subject: [PATCH 1/2] Improve SpecifiedTypes::inverse The previous assumption was incorrect, it is not enough to just swap the sureTypes and sureNotTypes, as this can easily lead to a NeverType. In this change a SubtractableType is created instead for both which makes retains more type information in follow-up operations like union or intersect. --- src/Analyser/SpecifiedTypes.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index 0feb361336..524a451e6f 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -3,8 +3,11 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\Type\MixedType; +use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; class SpecifiedTypes { @@ -122,7 +125,22 @@ public function unionWith(SpecifiedTypes $other): self public function inverse(): self { - return new self($this->sureNotTypes, $this->sureTypes, $this->overwrite, $this->newConditionalExpressionHolders); + $normalized = $this->normalize(); + + $inverseType = static function (Type $subtractedType) { + if ($subtractedType instanceof SubtractableType && $subtractedType->getSubtractedType() !== null) { + return TypeCombinator::union($subtractedType->getTypeWithoutSubtractedType(), $subtractedType->getSubtractedType()); + } + + return new MixedType(false, $subtractedType); + }; + + return new self( + array_map(static fn (array $sureType) => [$sureType[0], $inverseType($sureType[1])], $normalized->sureTypes), + array_map(static fn (array $sureNotType) => [$sureNotType[0], $inverseType($sureNotType[1])], $normalized->sureNotTypes), + $normalized->overwrite, + $normalized->newConditionalExpressionHolders, + ); } private function normalize(): self From 2237c47446c448a67a2fa27e02f090c2b644812e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 21 Feb 2022 21:09:49 +0100 Subject: [PATCH 2/2] 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 | 44 +++++++++++++++++++ tests/PHPStan/Analyser/data/instanceof.php | 2 +- .../data/root-scope-maybe-defined.php | 2 +- tests/PHPStan/Type/TypeCombinatorTest.php | 16 +++++++ 7 files changed, 80 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 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', + ]; } /**