Skip to content

Commit

Permalink
Early termination for NeverType match expression
Browse files Browse the repository at this point in the history
  • Loading branch information
rajyan authored and ondrejmirtes committed Mar 22, 2022
1 parent c564943 commit 99951ac
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 27 deletions.
4 changes: 4 additions & 0 deletions src/Type/Accessory/HasMethodType.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
return $otherType->isSuperTypeOf($this);
}

if ($this->isCallable()->yes() && $otherType->isCallable()->yes()) {
return TrinaryLogic::createYes();
}

if ($otherType instanceof self) {
$limit = TrinaryLogic::createYes();
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof CompoundType && !$type instanceof self) {
return $type->isSubTypeOf($this);
}

return $this->isSuperTypeOfInternal($type, false);
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/ClosureType.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}

return $this->isSuperTypeOfInternal($type, false);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}

return $this->isSuperTypeOfInternal($type, false);
}

private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}

$nakedSuperTypeOf = parent::isSuperTypeOf($type);
if ($nakedSuperTypeOf->no()) {
return $nakedSuperTypeOf;
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Generic/TemplateTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\SubtractableType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
Expand Down Expand Up @@ -200,6 +201,10 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
return $type->isSubTypeOf($this);
}

if ($type instanceof NeverType) {
return TrinaryLogic::createYes();
}

return $this->getBound()->isSuperTypeOf($type)
->and(TrinaryLogic::createMaybe());
}
Expand Down
4 changes: 4 additions & 0 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
return TrinaryLogic::createYes();
}

if ($otherType instanceof NeverType) {
return TrinaryLogic::createYes();
}

$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $innerType->isSuperTypeOf($otherType);
Expand Down
4 changes: 4 additions & 0 deletions src/Type/IterableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}

return $type->isIterable()
->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType()))
->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}

$keyType = TypeCombinator::union(...$keyTypes);
if ($keyType instanceof NeverType && !$keyType->isExplicit()) {
if ($keyType instanceof NeverType) {
return new ConstantArrayType([], []);
}

Expand Down
14 changes: 2 additions & 12 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,6 @@ public static function union(Type ...$types): Type
$scalarTypes = [];
$hasGenericScalarTypes = [];
for ($i = 0; $i < $typesCount; $i++) {
if ($types[$i] instanceof NeverType) {
unset($types[$i]);
continue;
}
if ($types[$i] instanceof ConstantScalarType) {
$type = $types[$i];
$scalarTypes[get_class($type)][md5($type->describe(VerbosityLevel::cache()))] = $type;
Expand Down Expand Up @@ -378,17 +374,11 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
}
}

if (
!$b instanceof ConstantArrayType
&& $b->isSuperTypeOf($a)->yes()
) {
if ($b->isSuperTypeOf($a)->yes()) {
return [null, $b];
}

if (
!$a instanceof ConstantArrayType
&& $a->isSuperTypeOf($b)->yes()
) {
if ($a->isSuperTypeOf($b)->yes()) {
return [$a, null];
}

Expand Down
1 change: 1 addition & 0 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic
if (
($otherType instanceof self && !$otherType instanceof TemplateUnionType)
|| $otherType instanceof IterableType
|| $otherType instanceof NeverType
) {
return $otherType->isSubTypeOf($this);
}
Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,10 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/curl_getinfo_7.3.php');
}

if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6251.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php');
Expand Down
63 changes: 63 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6251.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug6251;

use function PHPStan\Testing\assertType;

class Foo
{
function foo()
{
$var = 1;
if (rand(0, 1)) {
match(1) {
1 => throw new \Exception(),
};
} else {
$var = 2;
}
assertType('2', $var);
}

function bar($a): void
{
$var = 1;
if (rand(0, 1)) {
match($a) {
'a' => throw new \Error(),
default => throw new \Exception(),
};
} else {
$var = 2;
}
assertType('2', $var);
}

function baz($a): void
{
$var = 1;
if (rand(0, 1)) {
match($a) {
'a' => throw new \Error(),
// throws UnhandledMatchError if not handled
};
} else {
$var = 2;
}
assertType('2', $var);
}

function buz($a): void
{
$var = 1;
if (rand(0, 1)) {
match($a) {
'a' => throw new \Exception(),
default => var_dump($a),
};
} else {
$var = 2;
}
assertType('1|2', $var);
}
}
12 changes: 6 additions & 6 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,17 @@ public function testRule(): void
'Match expression does not handle remaining value: 3',
50,
],
[
'Match expression does not handle remaining values: 1|2|3',
55,
],
[
'Match arm comparison between 1|2 and 3 is always false.',
65,
61,
],
[
'Match arm comparison between 1 and 1 is always true.',
70,
66,
],
[
'Match expression does not handle remaining values: 1|2|3',
78,
],
[
'Match arm comparison between true and false is always false.',
Expand Down
8 changes: 4 additions & 4 deletions tests/PHPStan/Rules/Comparison/data/match-expr.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ public function doFoo(int $i): void
// unhandled
};

match ($i) {
// unhandled
};

match ($i) {
1, 2 => null,
default => null, // OK
Expand All @@ -78,6 +74,10 @@ public function doFoo(int $i): void
default => 1,
1 => 2,
};

match ($i) {
// unhandled
};
}

public function doBar(\Exception $e): void
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,19 @@ public function testModelMixin(bool $checkExplicitMixedMissingReturn): void
]);
}

public function testBug6257(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0.');
}
$this->checkExplicitMixedMissingReturn = true;
$this->checkPhpDocMissingReturn = true;
$this->analyse([__DIR__ . '/data/bug-6257.php'], [
[
'Function ReturnTypes\sometimesThrows() should always throw an exception or terminate script execution but doesn\'t do that.',
27,
],
]);
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Missing/data/bug-6257.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php // lint >= 8.0

namespace ReturnTypes;

/**
* @return never
*/
function alwaysThrow() {
match(true) {
true => throw new \Exception(),
};
}

/**
* @return never
*/
function alwaysThrow2() {
match(rand(0, 1)) {
0 => throw new \Exception(),
};
}

/**
* @return never
*/
function sometimesThrows() {
match(rand(0, 1)) {
0 => throw new \Exception(),
default => 'test',
};
}

0 comments on commit 99951ac

Please sign in to comment.