Skip to content

Commit 7366df9

Browse files
committed
Fix type narrowing in match with ternary subject like $x ? $x::class : null
- Added ternary expression decomposition in TypeSpecifier::resolveNormalizedIdentical - When ($cond ? $trueExpr : $falseExpr) === $value and only one branch can produce the value, narrows the condition and the matching branch independently - Evaluates the true branch type in the truthy-narrowed scope so that e.g. $request::class is computed with $request narrowed to non-null - New regression test in tests/PHPStan/Analyser/nsrt/bug-11923.php Closes phpstan/phpstan#11923
1 parent d462113 commit 7366df9

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,6 +2400,54 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
24002400

24012401
$rightType = $scope->getType($rightExpr);
24022402

2403+
// ($cond ? $trueExpr : $falseExpr) === $value
2404+
if (
2405+
$context->true()
2406+
&& $unwrappedLeftExpr instanceof Expr\Ternary
2407+
&& $unwrappedLeftExpr->if !== null
2408+
) {
2409+
$ternaryIf = $unwrappedLeftExpr->if;
2410+
$ternaryElse = $unwrappedLeftExpr->else;
2411+
$ternaryCond = $unwrappedLeftExpr->cond;
2412+
$falseBranchType = $scope->getType($ternaryElse);
2413+
// Evaluate true branch in scope where ternary condition is truthy,
2414+
// so that e.g. $request::class is evaluated with $request narrowed to non-null
2415+
$truthyScope = $scope->filterByTruthyValue($ternaryCond);
2416+
$trueBranchType = $truthyScope->getType($ternaryIf);
2417+
2418+
if ($rightType->isSuperTypeOf($falseBranchType)->no()) {
2419+
// Only the true branch can produce a value === $rightType
2420+
// Therefore $cond is truthy AND $trueExpr === $value
2421+
$condTruthyTypes = $this->specifyTypesInCondition(
2422+
$scope,
2423+
$ternaryCond,
2424+
TypeSpecifierContext::createTruthy(),
2425+
);
2426+
$trueNarrowingTypes = $this->resolveIdentical(
2427+
new Expr\BinaryOp\Identical($ternaryIf, $rightExpr),
2428+
$scope,
2429+
$context,
2430+
);
2431+
return $condTruthyTypes->unionWith($trueNarrowingTypes)->setRootExpr($expr);
2432+
}
2433+
2434+
if ($rightType->isSuperTypeOf($trueBranchType)->no()) {
2435+
// Only the false branch can produce a value === $rightType
2436+
// Therefore $cond is falsy AND $falseExpr === $value
2437+
$condFalsyTypes = $this->specifyTypesInCondition(
2438+
$scope,
2439+
$ternaryCond,
2440+
TypeSpecifierContext::createFalsey(),
2441+
);
2442+
$falseNarrowingTypes = $this->resolveIdentical(
2443+
new Expr\BinaryOp\Identical($ternaryElse, $rightExpr),
2444+
$scope,
2445+
$context,
2446+
);
2447+
return $condFalsyTypes->unionWith($falseNarrowingTypes)->setRootExpr($expr);
2448+
}
2449+
}
2450+
24032451
// (count($a) === $expr)
24042452
if (
24052453
!$context->null()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11923;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
final readonly class RequestA
10+
{
11+
/**
12+
* @param non-empty-string $phoneNumber
13+
*/
14+
public function __construct(
15+
public string $phoneNumber,
16+
public \DateTimeImmutable $birthAt,
17+
) {
18+
}
19+
}
20+
21+
final readonly class RequestB
22+
{
23+
/**
24+
* @param non-empty-string $passport
25+
*/
26+
public function __construct(
27+
public string $passport,
28+
public \DateTimeImmutable $birthAt,
29+
) {
30+
}
31+
}
32+
33+
function testNullableTernaryMatchSubject(?object $request): void
34+
{
35+
match ($request ? $request::class : null) {
36+
null => assertType('null', $request),
37+
RequestA::class => assertType(RequestA::class, $request),
38+
RequestB::class => assertType(RequestB::class, $request),
39+
};
40+
}
41+
42+
function testNonNullableMatchSubject(object $request): void
43+
{
44+
match ($request::class) {
45+
RequestA::class => assertType(RequestA::class, $request),
46+
RequestB::class => assertType(RequestB::class, $request),
47+
};
48+
}

0 commit comments

Comments
 (0)