Skip to content

Commit d925bdd

Browse files
committed
Fix phpstan/phpstan#14396: False UnhandledMatchError in foreach with treatPhpDocTypesAsCertain=false
- Fixed TypeSpecifier to specify types for both AlwaysRememberedExpr and its unwrapped inner expression when handling always-true/false identity comparisons - Previously, only the unwrapped expression was specified, leaving the AlwaysRememberedExpr type un-narrowed in the match scope - New regression test in tests/PHPStan/Rules/Exceptions/Bug14396Test.php
1 parent 4c6ef6e commit d925bdd

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2998,12 +2998,14 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29982998
$never = new NeverType();
29992999
$contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
30003000
if ($leftExpr instanceof AlwaysRememberedExpr) {
3001-
$leftTypes = $this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3001+
$leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr)
3002+
->unionWith($this->create($unwrappedLeftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr));
30023003
} else {
30033004
$leftTypes = $this->create($leftExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
30043005
}
30053006
if ($rightExpr instanceof AlwaysRememberedExpr) {
3006-
$rightTypes = $this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
3007+
$rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr)
3008+
->unionWith($this->create($unwrappedRightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr));
30073009
} else {
30083010
$rightTypes = $this->create($rightExpr, $never, $contextForTypes, $scope)->setRootExpr($expr);
30093011
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\RequiresPhp;
8+
9+
/**
10+
* @extends RuleTestCase<MissingCheckedExceptionInFunctionThrowsRule>
11+
*/
12+
class Bug14396Test extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new MissingCheckedExceptionInFunctionThrowsRule(
18+
new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver(
19+
self::createReflectionProvider(),
20+
[],
21+
[],
22+
[],
23+
[],
24+
)),
25+
);
26+
}
27+
28+
protected function shouldTreatPhpDocTypesAsCertain(): bool
29+
{
30+
return false;
31+
}
32+
33+
#[RequiresPhp('>= 8.1')]
34+
public function testRule(): void
35+
{
36+
$this->analyse([__DIR__ . '/data/bug-14396.php'], []);
37+
}
38+
39+
public static function getAdditionalConfigFiles(): array
40+
{
41+
return [
42+
__DIR__ . '/bug-14396.neon',
43+
];
44+
}
45+
46+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
treatPhpDocTypesAsCertain: false
3+
exceptions:
4+
check:
5+
missingCheckedExceptionInThrows: true
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug14396;
6+
7+
enum Status {
8+
case A;
9+
case B;
10+
case C;
11+
}
12+
13+
class Item {
14+
public function __construct(
15+
public ?Status $status
16+
) {}
17+
}
18+
19+
/**
20+
* @param list<Item> $list
21+
*/
22+
function countAFromCollection(array $list): int
23+
{
24+
$count = 0;
25+
26+
foreach ($list as $item) {
27+
match ($item->status) {
28+
Status::A => ++$count,
29+
Status::B,
30+
Status::C,
31+
null => null,
32+
};
33+
}
34+
35+
return $count;
36+
}
37+
38+
function countAFromItem(Item $item): ?int {
39+
return match ($item->status) {
40+
Status::A => 1,
41+
Status::B,
42+
Status::C,
43+
null => null,
44+
};
45+
}

0 commit comments

Comments
 (0)