Skip to content

Commit a769a1c

Browse files
committed
TypeSpecifier - handle AlwaysRememberedExpr in handling Identical
1 parent 987fb32 commit a769a1c

File tree

5 files changed

+169
-44
lines changed

5 files changed

+169
-44
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
16451645
if ($leftExpr instanceof AlwaysRememberedExpr) {
16461646
$unwrappedLeftExpr = $leftExpr->getExpr();
16471647
}
1648+
$unwrappedRightExpr = $rightExpr;
1649+
if ($rightExpr instanceof AlwaysRememberedExpr) {
1650+
$unwrappedRightExpr = $rightExpr->getExpr();
1651+
}
16481652
$rightType = $scope->getType($rightExpr);
16491653
if (
16501654
$context->true()
@@ -1695,124 +1699,151 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
16951699

16961700
$specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr);
16971701
if ($specifiedType !== null) {
1702+
if ($exprNode instanceof AlwaysRememberedExpr) {
1703+
$specifiedType->unionWith(
1704+
$this->create($exprNode->getExpr(), $constantType, $context, false, $scope, $rootExpr),
1705+
);
1706+
}
16981707
return $specifiedType;
16991708
}
17001709
}
17011710

1702-
if ($rightExpr instanceof AlwaysRememberedExpr) {
1703-
$rightExpr = $rightExpr->getExpr();
1704-
}
1705-
1706-
if ($leftExpr instanceof AlwaysRememberedExpr) {
1707-
$leftExpr = $leftExpr->getExpr();
1708-
}
1709-
17101711
if (
17111712
$context->true() &&
1712-
$leftExpr instanceof ClassConstFetch &&
1713-
$leftExpr->class instanceof Expr &&
1714-
$leftExpr->name instanceof Node\Identifier &&
1715-
$rightExpr instanceof ClassConstFetch &&
1713+
$unwrappedLeftExpr instanceof ClassConstFetch &&
1714+
$unwrappedLeftExpr->class instanceof Expr &&
1715+
$unwrappedLeftExpr->name instanceof Node\Identifier &&
1716+
$unwrappedRightExpr instanceof ClassConstFetch &&
17161717
$rightType instanceof ConstantStringType &&
1717-
strtolower($leftExpr->name->toString()) === 'class'
1718+
strtolower($unwrappedLeftExpr->name->toString()) === 'class'
17181719
) {
17191720
return $this->specifyTypesInCondition(
17201721
$scope,
17211722
new Instanceof_(
1722-
$leftExpr->class,
1723+
$unwrappedLeftExpr->class,
17231724
new Name($rightType->getValue()),
17241725
),
17251726
$context,
17261727
$rootExpr,
1727-
)->unionWith($this->create($expr->left, $rightType, $context, false, $scope, $rootExpr));
1728+
)->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr));
17281729
}
17291730

17301731
$leftType = $scope->getType($leftExpr);
17311732
if (
17321733
$context->true() &&
1733-
$rightExpr instanceof ClassConstFetch &&
1734-
$rightExpr->class instanceof Expr &&
1735-
$rightExpr->name instanceof Node\Identifier &&
1736-
$leftExpr instanceof ClassConstFetch &&
1734+
$unwrappedRightExpr instanceof ClassConstFetch &&
1735+
$unwrappedRightExpr->class instanceof Expr &&
1736+
$unwrappedRightExpr->name instanceof Node\Identifier &&
1737+
$unwrappedLeftExpr instanceof ClassConstFetch &&
17371738
$leftType instanceof ConstantStringType &&
1738-
strtolower($rightExpr->name->toString()) === 'class'
1739+
strtolower($unwrappedRightExpr->name->toString()) === 'class'
17391740
) {
17401741
return $this->specifyTypesInCondition(
17411742
$scope,
17421743
new Instanceof_(
1743-
$rightExpr->class,
1744+
$unwrappedRightExpr->class,
17441745
new Name($leftType->getValue()),
17451746
),
17461747
$context,
17471748
$rootExpr,
1748-
)->unionWith($this->create($expr->right, $leftType, $context, false, $scope, $rootExpr));
1749+
)->unionWith($this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr));
17491750
}
17501751

17511752
if ($context->false()) {
17521753
$identicalType = $scope->getType($expr);
17531754
if ($identicalType instanceof ConstantBooleanType) {
17541755
$never = new NeverType();
17551756
$contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
1756-
$leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope, $rootExpr);
1757-
$rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope, $rootExpr);
1757+
$leftTypes = $this->create($leftExpr, $never, $contextForTypes, false, $scope, $rootExpr)
1758+
->unionWith($this->create($unwrappedLeftExpr, $never, $contextForTypes, false, $scope, $rootExpr));
1759+
$rightTypes = $this->create($rightExpr, $never, $contextForTypes, false, $scope, $rootExpr)
1760+
->unionWith($this->create($unwrappedRightExpr, $never, $contextForTypes, false, $scope, $rootExpr));
17581761
return $leftTypes->unionWith($rightTypes);
17591762
}
17601763
}
17611764

17621765
$types = null;
1763-
$exprLeftType = $scope->getType($expr->left);
1764-
$exprRightType = $scope->getType($expr->right);
17651766
if (
1766-
count($exprLeftType->getFiniteTypes()) === 1
1767-
|| ($exprLeftType->isConstantValue()->yes() && !$exprRightType->equals($exprLeftType) && $exprRightType->isSuperTypeOf($exprLeftType)->yes())
1767+
count($leftType->getFiniteTypes()) === 1
1768+
|| ($leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes())
17681769
) {
17691770
$types = $this->create(
1770-
$expr->right,
1771-
$exprLeftType,
1771+
$rightExpr,
1772+
$leftType,
17721773
$context,
17731774
false,
17741775
$scope,
17751776
$rootExpr,
17761777
);
1778+
if ($rightExpr instanceof AlwaysRememberedExpr) {
1779+
$types = $types->unionWith($this->create(
1780+
$unwrappedRightExpr,
1781+
$leftType,
1782+
$context,
1783+
false,
1784+
$scope,
1785+
$rootExpr,
1786+
));
1787+
}
17771788
}
17781789
if (
1779-
count($exprRightType->getFiniteTypes()) === 1
1780-
|| ($exprRightType->isConstantValue()->yes() && !$exprLeftType->equals($exprRightType) && $exprLeftType->isSuperTypeOf($exprRightType)->yes())
1790+
count($rightType->getFiniteTypes()) === 1
1791+
|| ($rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes())
17811792
) {
1782-
$leftType = $this->create(
1783-
$expr->left,
1784-
$exprRightType,
1793+
$leftTypes = $this->create(
1794+
$leftExpr,
1795+
$rightType,
17851796
$context,
17861797
false,
17871798
$scope,
17881799
$rootExpr,
17891800
);
1801+
if ($leftExpr instanceof AlwaysRememberedExpr) {
1802+
$leftTypes = $leftTypes->unionWith($this->create(
1803+
$unwrappedLeftExpr,
1804+
$rightType,
1805+
$context,
1806+
false,
1807+
$scope,
1808+
$rootExpr,
1809+
));
1810+
}
17901811
if ($types !== null) {
1791-
$types = $types->unionWith($leftType);
1812+
$types = $types->unionWith($leftTypes);
17921813
} else {
1793-
$types = $leftType;
1814+
$types = $leftTypes;
17941815
}
17951816
}
17961817

17971818
if ($types !== null) {
17981819
return $types;
17991820
}
18001821

1801-
$leftExprString = $this->exprPrinter->printExpr($expr->left);
1802-
$rightExprString = $this->exprPrinter->printExpr($expr->right);
1822+
$leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr);
1823+
$rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr);
18031824
if ($leftExprString === $rightExprString) {
1804-
if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
1825+
if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) {
18051826
return new SpecifiedTypes([], [], false, [], $rootExpr);
18061827
}
18071828
}
18081829

18091830
if ($context->true()) {
1810-
$leftTypes = $this->create($expr->left, $exprRightType, $context, false, $scope, $rootExpr);
1811-
$rightTypes = $this->create($expr->right, $exprLeftType, $context, false, $scope, $rootExpr);
1831+
$leftTypes = $this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr);
1832+
$rightTypes = $this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr);
1833+
if ($leftExpr instanceof AlwaysRememberedExpr) {
1834+
$leftTypes = $leftTypes->unionWith(
1835+
$this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr),
1836+
);
1837+
}
1838+
if ($rightExpr instanceof AlwaysRememberedExpr) {
1839+
$rightTypes = $rightTypes->unionWith(
1840+
$this->create($unwrappedRightExpr, $leftType, $context, false, $scope, $rootExpr),
1841+
);
1842+
}
18121843
return $leftTypes->unionWith($rightTypes);
18131844
} elseif ($context->false()) {
1814-
return $this->create($expr->left, $exprLeftType, $context, false, $scope, $rootExpr)->normalize($scope)
1815-
->intersectWith($this->create($expr->right, $exprRightType, $context, false, $scope, $rootExpr)->normalize($scope));
1845+
return $this->create($leftExpr, $leftType, $context, false, $scope, $rootExpr)->normalize($scope)
1846+
->intersectWith($this->create($rightExpr, $rightType, $context, false, $scope, $rootExpr)->normalize($scope));
18161847
}
18171848

18181849
return new SpecifiedTypes([], [], false, [], $rootExpr);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,7 @@ public function dataFileAsserts(): iterable
12601260
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8827.php');
12611261
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4907.php');
12621262
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8924.php');
1263+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9499.php');
12631264
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5998.php');
12641265
yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php');
12651266
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php');

tests/PHPStan/Analyser/TypeSpecifierTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser;
44

5+
use Bug9499\FooEnum;
56
use PhpParser\Node\Arg;
67
use PhpParser\Node\Expr;
78
use PhpParser\Node\Expr\BinaryOp\Equal;
@@ -18,6 +19,7 @@
1819
use PhpParser\Node\Scalar\String_;
1920
use PhpParser\Node\VarLikeIdentifier;
2021
use PhpParser\PrettyPrinter\Standard;
22+
use PHPStan\Node\Expr\AlwaysRememberedExpr;
2123
use PHPStan\Node\Printer\Printer;
2224
use PHPStan\Testing\PHPStanTestCase;
2325
use PHPStan\Type\ArrayType;
@@ -1211,6 +1213,36 @@ public function dataCondition(): array
12111213
],
12121214
[],
12131215
],
1216+
[
1217+
new Identical(
1218+
new PropertyFetch(new Variable('foo'), 'bar'),
1219+
new Expr\ClassConstFetch(new Name(FooEnum::class), 'A'),
1220+
),
1221+
[
1222+
'$foo->bar' => 'Bug9499\FooEnum::A',
1223+
],
1224+
[
1225+
'$foo->bar' => '~Bug9499\FooEnum::A',
1226+
],
1227+
],
1228+
[
1229+
new Identical(
1230+
new AlwaysRememberedExpr(
1231+
new PropertyFetch(new Variable('foo'), 'bar'),
1232+
new ObjectType(FooEnum::class),
1233+
new ObjectType(FooEnum::class),
1234+
),
1235+
new Expr\ClassConstFetch(new Name(FooEnum::class), 'A'),
1236+
),
1237+
[
1238+
'__phpstanRembered($foo->bar)' => 'Bug9499\FooEnum::A',
1239+
'$foo->bar' => 'Bug9499\FooEnum::A',
1240+
],
1241+
[
1242+
'__phpstanRembered($foo->bar)' => '~Bug9499\FooEnum::A',
1243+
'$foo->bar' => '~Bug9499\FooEnum::A',
1244+
],
1245+
],
12141246
];
12151247
}
12161248

tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,4 +516,13 @@ public function testBug8536(): void
516516
$this->analyse([__DIR__ . '/data/bug-8536.php'], []);
517517
}
518518

519+
public function testBug9499(): void
520+
{
521+
if (PHP_VERSION_ID < 80100) {
522+
$this->markTestSkipped('Test requires PHP 8.1.');
523+
}
524+
525+
$this->analyse([__DIR__ . '/data/bug-9499.php'], []);
526+
}
527+
519528
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug9499;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
enum FooEnum
8+
{
9+
case A;
10+
case B;
11+
case C;
12+
case D;
13+
}
14+
15+
class Foo
16+
{
17+
public function __construct(public readonly FooEnum $f)
18+
{
19+
}
20+
}
21+
22+
function test(FooEnum $f, Foo $foo): void
23+
{
24+
$arr = ['f' => $f];
25+
match ($arr['f']) {
26+
FooEnum::A, FooEnum::B => match ($arr['f']) {
27+
FooEnum::A => 'a',
28+
FooEnum::B => 'b',
29+
},
30+
default => '',
31+
};
32+
match ($foo->f) {
33+
FooEnum::A, FooEnum::B => match ($foo->f) {
34+
FooEnum::A => 'a',
35+
FooEnum::B => 'b',
36+
},
37+
default => '',
38+
};
39+
}
40+
41+
function test2(FooEnum $f, Foo $foo): void
42+
{
43+
$arr = ['f' => $f];
44+
match ($arr['f']) {
45+
FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $arr['f']),
46+
default => '',
47+
};
48+
match ($foo->f) {
49+
FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $foo->f),
50+
default => '',
51+
};
52+
}

0 commit comments

Comments
 (0)