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 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', + ]; } /**