Skip to content

Commit

Permalink
Narrow variable type in switch cases
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytro-dymarchuk committed Jan 19, 2025
1 parent bf923ad commit 7a01578
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 10 deletions.
31 changes: 31 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
use function array_merge;
use function array_pop;
use function array_reverse;
use function array_shift;
use function array_slice;
use function array_values;
use function base64_decode;
Expand Down Expand Up @@ -1513,9 +1514,11 @@ private function processStmtNode(
$exitPointsForOuterLoop = [];
$throwPoints = $condResult->getThrowPoints();
$impurePoints = $condResult->getImpurePoints();
$defaultCondExprs = [];
foreach ($stmt->cases as $caseNode) {
if ($caseNode->cond !== null) {
$condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
$defaultCondExprs[] = new BinaryOp\NotEqual($stmt->cond, $caseNode->cond);
$caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep());
$scopeForBranches = $caseResult->getScope();
$hasYield = $hasYield || $caseResult->hasYield();
Expand All @@ -1525,6 +1528,11 @@ private function processStmtNode(
} else {
$hasDefaultCase = true;
$branchScope = $scopeForBranches;
$defaultConditions = $this->createBooleanAndFromExpressions($defaultCondExprs);
if ($defaultConditions !== null) {
$branchScope = $this->processExprNode($stmt, $defaultConditions, $scope, static function (): void {
}, ExpressionContext::createDeep())->getTruthyScope()->filterByTruthyValue($defaultConditions);
}
}

$branchScope = $branchScope->mergeWith($prevScope);
Expand Down Expand Up @@ -6548,6 +6556,29 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $
return null;
}

/**
* @param list<Expr> $expressions
*/
private function createBooleanAndFromExpressions(array $expressions): ?Expr
{
if (count($expressions) === 0) {
return null;
}

if (count($expressions) === 1) {
return $expressions[0];
}

$left = array_shift($expressions);
$right = $this->createBooleanAndFromExpressions($expressions);

if ($right === null) {
throw new ShouldNotHappenException();
}

return new BooleanAnd($left, $right);
}

/**
* @param array<Node> $nodes
* @return list<Node\Stmt>
Expand Down
66 changes: 57 additions & 9 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\Enum\EnumCaseObjectType;
use PHPStan\Type\FloatType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
Expand Down Expand Up @@ -1572,15 +1573,8 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\
$leftType = $scope->getType($binaryOperation->left);
$rightType = $scope->getType($binaryOperation->right);

$rightExpr = $binaryOperation->right;
if ($rightExpr instanceof AlwaysRememberedExpr) {
$rightExpr = $rightExpr->getExpr();
}

$leftExpr = $binaryOperation->left;
if ($leftExpr instanceof AlwaysRememberedExpr) {
$leftExpr = $leftExpr->getExpr();
}
$rightExpr = $this->extractExpression($binaryOperation->right);
$leftExpr = $this->extractExpression($binaryOperation->left);

if (
$leftType instanceof ConstantScalarType
Expand All @@ -1599,6 +1593,39 @@ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\
return null;
}

/**
* @return array{Expr, EnumCaseObjectType, Type}|null
*/
private function findEnumTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array
{
$leftType = $scope->getType($binaryOperation->left);
$rightType = $scope->getType($binaryOperation->right);

$rightExpr = $this->extractExpression($binaryOperation->right);
$leftExpr = $this->extractExpression($binaryOperation->left);

if (
$leftType->getEnumCases() === [$leftType]
&& !$rightExpr instanceof ConstFetch
&& !$rightExpr instanceof ClassConstFetch
) {
return [$binaryOperation->right, $leftType, $rightType];

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.1)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1612 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.
} elseif (
$rightType->getEnumCases() === [$rightType]
&& !$leftExpr instanceof ConstFetch
&& !$leftExpr instanceof ClassConstFetch
) {
return [$binaryOperation->left, $rightType, $leftType];

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.1)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.

Check failure on line 1618 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Method PHPStan\Analyser\TypeSpecifier::findEnumTypeExpressionsFromBinaryOperation() should return array{PhpParser\Node\Expr, PHPStan\Type\Enum\EnumCaseObjectType, PHPStan\Type\Type}|null but returns array{PhpParser\Node\Expr, PHPStan\Type\Type, PHPStan\Type\Type}.
}

return null;
}

private function extractExpression(Expr $expr): Expr
{
return $expr instanceof AlwaysRememberedExpr ? $expr->getExpr() : $expr;
}

/** @api */
public function create(
Expr $expr,
Expand Down Expand Up @@ -1990,6 +2017,27 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
) {
return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context)->setRootExpr($expr);
}

if (!$context->null() && TypeCombinator::containsNull($otherType)) {
if ($constantType->toBoolean()->isTrue()->yes()) {
$otherType = TypeCombinator::remove($otherType, new NullType());
}

if (!$otherType->isSuperTypeOf($constantType)->no()) {
return $this->create($exprNode, TypeCombinator::intersect($constantType, $otherType), $context, $scope)->setRootExpr($expr);
}
}
}

$expressions = $this->findEnumTypeExpressionsFromBinaryOperation($scope, $expr);
if ($expressions !== null) {
$exprNode = $expressions[0];
$constantType = $expressions[1];
$otherType = $expressions[2];

if (!$context->null()) {
return $this->create($exprNode, TypeCombinator::intersect($constantType, $otherType), $context, $scope)->setRootExpr($expr);
}
}

$leftType = $scope->getType($expr->left);
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ private static function findTestFiles(): iterable
yield __DIR__ . '/data/new-in-initializers-runtime.php';
}

yield __DIR__ . '/data/bug-12432-nullable-int.php';

yield __DIR__ . '/../Rules/Comparison/data/bug-6473.php';

yield __DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php';
Expand Down
35 changes: 35 additions & 0 deletions tests/PHPStan/Analyser/data/bug-12432-nullable-enum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Bug12432;

use function PHPStan\Testing\assertType;

enum Foo: int
{
case BAR = 1;
case BAZ = 2;
case QUX = 3;
}

function requireNullableEnum(?Foo $nullable): ?Foo
{
switch ($nullable) {
case Foo::BAR:
assertType('Bug12432\Foo::BAR', $nullable);
case Foo::BAZ:
assertType('Bug12432\Foo::BAR|Bug12432\Foo::BAZ', $nullable);
break;
case '':
assertType('null', $nullable);
case null:
assertType('null', $nullable);
break;
case 0:
assertType('*NEVER*', $nullable);
default:
assertType('Bug12432\Foo::QUX', $nullable);
break;
}

return $nullable;
}
26 changes: 26 additions & 0 deletions tests/PHPStan/Analyser/data/bug-12432-nullable-int.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Bug12432;

use function PHPStan\Testing\assertType;

function requireNullableInt(?int $nullable): ?int
{
switch ($nullable) {
case 1:
assertType('1', $nullable);
case 2:
assertType('1|2', $nullable);
break;
case '':
assertType('0|null', $nullable);
case 0:
assertType('0|null', $nullable);
break;
default:
assertType('int<min, -1>|int<3, max>', $nullable);
break;
}

return $nullable;
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/in_array_loose.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function looseComparison(
assertType('int|string', $stringOrInt); // could be '1'|'2'|1|2
}
if (in_array($stringOrNull, ['1', 'a'])) {
assertType('string|null', $stringOrNull); // could be '1'|'a'
assertType("'1'|'a'", $stringOrNull);
}
}
}

0 comments on commit 7a01578

Please sign in to comment.