Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve type specification of subtractable types #1028

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/Analyser/SpecifiedTypes.php
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/Type/TypeCombinator.php
Expand Up @@ -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];
Expand All @@ -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];
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Expand Up @@ -697,7 +697,7 @@ public function dataAssignInIf(): array
$testScope,
'mixed',
TrinaryLogic::createYes(),
'mixed', // should be mixed~bool+1
'mixed~bool',
],
[
$testScope,
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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');
}

/**
Expand Down
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6672.php
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace Bug6672;

use function PHPStan\Testing\assertType;

function foo(int $a, ?int $b, int $c, ?int $d, ?int $e, ?int $f): void
{
if ($a > 17) {
assertType('int<18, max>', $a);
} else {
assertType('int<min, 17>', $a);
}

if ($b > 17 || $b === null) {
assertType('int<18, max>|null', $b);
} else {
assertType('int<min, 17>', $b);
}

if ($c < 17) {
assertType('int<min, 16>', $c);
} else {
assertType('int<17, max>', $c);
}

if ($d < 17 || $d === null) {
assertType('int<min, 16>|null', $d);
} else {
assertType('int<17, max>', $d);
}

if ($e >= 17 && $e <= 19 || $e === null) {
assertType('int<17, 19>|null', $e);
} else {
assertType('int<min, 16>|int<20, max>', $e);
}

if ($f < 17 || $f > 19 || $f === null) {
assertType('int<min, 16>|int<20, max>|null', $f);
} else {
assertType('int<17, 19>', $f);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/instanceof.php
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/root-scope-maybe-defined.php
Expand Up @@ -16,7 +16,7 @@
\PHPStan\Testing\assertType('1', $baz);
}

\PHPStan\Testing\assertType('mixed', $baz);
\PHPStan\Testing\assertType('mixed~null', $baz);
herndlm marked this conversation as resolved.
Show resolved Hide resolved

function () {
\PHPStan\Testing\assertVariableCertainty(TrinaryLogic::createNo(), $foo);
Expand Down
16 changes: 16 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Expand Up @@ -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',
];
}

/**
Expand Down Expand Up @@ -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',
];
}

/**
Expand Down