Skip to content

Commit 19b0f0b

Browse files
authored
Merge branch refs/heads/2.1.x into 2.2.x
2 parents 8c2b2b4 + 7b850e4 commit 19b0f0b

File tree

4 files changed

+105
-1
lines changed

4 files changed

+105
-1
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4402,6 +4402,8 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
44024402
}
44034403
}
44044404

4405+
$scopeForMatchNodeCallback = $scope;
4406+
44054407
$isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond;
44064408
if (!$isExhaustive) {
44074409
$remainingType = $matchScope->getType($expr->cond);
@@ -4429,7 +4431,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
44294431

44304432
ksort($armNodes, SORT_NUMERIC);
44314433

4432-
$this->callNodeCallback($nodeCallback, new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope, $storage);
4434+
$this->callNodeCallback($nodeCallback, new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scopeForMatchNodeCallback, $storage);
44334435

44344436
if ($expr->cond instanceof AlwaysRememberedExpr) {
44354437
$expr->cond = $expr->cond->getExpr();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\MatchExpressionNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\VerbosityLevel;
11+
12+
/**
13+
* This rule exists solely as a regression test for the match expression
14+
* callback scope fix. It reports the type of the match condition as seen
15+
* from the scope passed to the MatchExpressionNode callback.
16+
*
17+
* Without the fix, exhaustive match expressions pass the merged arm body
18+
* scope to the callback, which contains narrowed types from arm conditions
19+
* instead of the original match condition type.
20+
*
21+
* @implements Rule<MatchExpressionNode>
22+
*/
23+
final class MatchCallbackScopeRegressionRule implements Rule
24+
{
25+
26+
public function getNodeType(): string
27+
{
28+
return MatchExpressionNode::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
return [
34+
RuleErrorBuilder::message(
35+
$scope->getType($node->getCondition())->describe(VerbosityLevel::precise()),
36+
)->identifier('test.matchCallbackScope')->build(),
37+
];
38+
}
39+
40+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\RequiresPhp;
8+
9+
/**
10+
* Regression test: the scope passed to the MatchExpressionNode callback
11+
* must reflect the original match condition type, not the merged arm body
12+
* scope which contains narrowed types from individual arms.
13+
*
14+
* @extends RuleTestCase<MatchCallbackScopeRegressionRule>
15+
*/
16+
class MatchCallbackScopeRegressionTest extends RuleTestCase
17+
{
18+
19+
protected function getRule(): Rule
20+
{
21+
return new MatchCallbackScopeRegressionRule();
22+
}
23+
24+
#[RequiresPhp('>= 8.1')]
25+
public function testExhaustiveMatchCallbackScope(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/match-callback-scope-regression.php'], [
28+
[
29+
'MatchCallbackScopeRegression\Suit',
30+
23,
31+
],
32+
]);
33+
}
34+
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php // lint >= 8.1
2+
3+
namespace MatchCallbackScopeRegression;
4+
5+
enum Suit
6+
{
7+
case Hearts;
8+
case Diamonds;
9+
}
10+
11+
class Wrapper
12+
{
13+
14+
public function getSuit(): Suit
15+
{
16+
return Suit::Hearts;
17+
}
18+
19+
}
20+
21+
function exhaustiveMatchWithMethodCallReturningEnum(Wrapper $w): void
22+
{
23+
match ($w->getSuit()) {
24+
1 => null,
25+
default => null,
26+
};
27+
}

0 commit comments

Comments
 (0)