Skip to content

Commit ef8f124

Browse files
Narrow variable type in switch cases
1 parent bf923ad commit ef8f124

File tree

5 files changed

+110
-4
lines changed

5 files changed

+110
-4
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
use function array_merge;
205205
use function array_pop;
206206
use function array_reverse;
207+
use function array_shift;
207208
use function array_slice;
208209
use function array_values;
209210
use function base64_decode;
@@ -1513,9 +1514,11 @@ private function processStmtNode(
15131514
$exitPointsForOuterLoop = [];
15141515
$throwPoints = $condResult->getThrowPoints();
15151516
$impurePoints = $condResult->getImpurePoints();
1517+
$defaultCondExprs = [];
15161518
foreach ($stmt->cases as $caseNode) {
15171519
if ($caseNode->cond !== null) {
15181520
$condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
1521+
$defaultCondExprs[] = new BinaryOp\NotEqual($stmt->cond, $caseNode->cond);
15191522
$caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep());
15201523
$scopeForBranches = $caseResult->getScope();
15211524
$hasYield = $hasYield || $caseResult->hasYield();
@@ -1525,6 +1528,16 @@ private function processStmtNode(
15251528
} else {
15261529
$hasDefaultCase = true;
15271530
$branchScope = $scopeForBranches;
1531+
$defaultConditions = $this->createBooleanAndFromExpressions($defaultCondExprs);
1532+
if ($defaultConditions !== null) {
1533+
$branchScope = $this->processExprNode(
1534+
$stmt,
1535+
$defaultConditions,
1536+
$scope,
1537+
static function (): void {},
1538+
ExpressionContext::createDeep(),
1539+
)->getTruthyScope()->filterByTruthyValue($defaultConditions);
1540+
}
15281541
}
15291542

15301543
$branchScope = $branchScope->mergeWith($prevScope);
@@ -6548,6 +6561,24 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $
65486561
return null;
65496562
}
65506563

6564+
/**
6565+
* @param array<Expr> $expressions
6566+
*/
6567+
private function createBooleanAndFromExpressions(array $expressions): ?Expr {
6568+
if (count($expressions) === 0) {
6569+
return null;
6570+
}
6571+
6572+
if (count($expressions) === 1) {
6573+
return $expressions[0];
6574+
}
6575+
6576+
$left = array_shift($expressions);
6577+
$right = $this->createBooleanAndFromExpressions($expressions);
6578+
6579+
return new BooleanAnd($left, $right);
6580+
}
6581+
65516582
/**
65526583
* @param array<Node> $nodes
65536584
* @return list<Node\Stmt>

src/Analyser/TypeSpecifier.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use PHPStan\Type\Constant\ConstantIntegerType;
4949
use PHPStan\Type\Constant\ConstantStringType;
5050
use PHPStan\Type\ConstantScalarType;
51+
use PHPStan\Type\Enum\EnumCaseObjectType;
5152
use PHPStan\Type\FloatType;
5253
use PHPStan\Type\FunctionTypeSpecifyingExtension;
5354
use PHPStan\Type\Generic\GenericClassStringType;
@@ -1565,7 +1566,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy
15651566
}
15661567

15671568
/**
1568-
* @return array{Expr, ConstantScalarType, Type}|null
1569+
* @return array{Expr, ConstantScalarType|EnumCaseObjectType, Type}|null
15691570
*/
15701571
private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
15711572
{
@@ -1583,13 +1584,13 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\
15831584
}
15841585

15851586
if (
1586-
$leftType instanceof ConstantScalarType
1587+
($leftType instanceof ConstantScalarType || $leftType instanceof EnumCaseObjectType)
15871588
&& !$rightExpr instanceof ConstFetch
15881589
&& !$rightExpr instanceof ClassConstFetch
15891590
) {
15901591
return [$binaryOperation->right, $leftType, $rightType];
15911592
} elseif (
1592-
$rightType instanceof ConstantScalarType
1593+
($rightType instanceof ConstantScalarType || $rightType instanceof EnumCaseObjectType)
15931594
&& !$leftExpr instanceof ConstFetch
15941595
&& !$leftExpr instanceof ClassConstFetch
15951596
) {
@@ -1897,6 +1898,10 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
18971898
$constantType = $expressions[1];
18981899
$otherType = $expressions[2];
18991900

1901+
if (!$context->null() && $constantType instanceof EnumCaseObjectType) {
1902+
return $this->create($exprNode, TypeCombinator::intersect($constantType, $otherType), $context, $scope)->setRootExpr($expr);
1903+
}
1904+
19001905
if (!$context->null() && $constantType->getValue() === null) {
19011906
$trueTypes = [
19021907
new NullType(),
@@ -1990,6 +1995,17 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
19901995
) {
19911996
return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
19921997
}
1998+
1999+
if (!$context->null() && TypeCombinator::containsNull($otherType)) {
2000+
$boolType = $constantType->toBoolean();
2001+
if ($boolType instanceof ConstantBooleanType && $boolType->getValue()) {
2002+
$otherType = TypeCombinator::remove($otherType, new NullType());
2003+
}
2004+
2005+
if (!$otherType->isSuperTypeOf($constantType)->no()) {
2006+
return $this->create($exprNode, TypeCombinator::intersect($constantType, $otherType), $context, $scope)->setRootExpr($expr);
2007+
}
2008+
}
19932009
}
19942010

19952011
$leftType = $scope->getType($expr->left);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ private static function findTestFiles(): iterable
104104
yield __DIR__ . '/data/new-in-initializers-runtime.php';
105105
}
106106

107+
yield __DIR__ . '/data/bug-12432.php';
108+
107109
yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php';
108110

109111
yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Bug12432;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function requireNullableInt(?int $nullable): ?int
8+
{
9+
switch ($nullable) {
10+
case 1:
11+
assertType('1', $nullable);
12+
case 2:
13+
assertType('1|2', $nullable);
14+
break;
15+
case '':
16+
assertType('0|null', $nullable);
17+
case 0:
18+
assertType('0|null', $nullable);
19+
break;
20+
default:
21+
assertType('int<min, -1>|int<3, max>', $nullable);
22+
break;
23+
}
24+
25+
return $nullable;
26+
}
27+
28+
enum Foo: int
29+
{
30+
case BAR = 1;
31+
case BAZ = 2;
32+
case QUX = 3;
33+
}
34+
35+
function requireNullableEnum(?Foo $nullable): ?Foo
36+
{
37+
switch ($nullable) {
38+
case Foo::BAR:
39+
assertType('Bug12432\Foo::BAR', $nullable);
40+
case Foo::BAZ:
41+
assertType('Bug12432\Foo::BAR|Bug12432\Foo::BAZ', $nullable);
42+
break;
43+
case '':
44+
assertType('null', $nullable);
45+
case null:
46+
assertType('null', $nullable);
47+
break;
48+
case 0:
49+
assertType('*NEVER*', $nullable);
50+
default:
51+
assertType('Bug12432\Foo::QUX', $nullable);
52+
break;
53+
}
54+
55+
return $nullable;
56+
}
57+

tests/PHPStan/Analyser/nsrt/in_array_loose.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function looseComparison(
4242
assertType('int|string', $stringOrInt); // could be '1'|'2'|1|2
4343
}
4444
if (in_array($stringOrNull, ['1', 'a'])) {
45-
assertType('string|null', $stringOrNull); // could be '1'|'a'
45+
assertType("'1'|'a'", $stringOrNull);
4646
}
4747
}
4848
}

0 commit comments

Comments
 (0)