Skip to content

Commit 0fe9f46

Browse files
phpstan-botVincentLangletclaude
authored
Fix phpstan/phpstan#14446: Incorrect type-narrowing when using strict rules (#5435)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent a535a22 commit 0fe9f46

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
lines changed

src/Analyser/MutatingScope.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3738,7 +3738,7 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco
37383738
$finallyScope->nativeExpressionTypes,
37393739
$originalFinallyScope->nativeExpressionTypes,
37403740
),
3741-
$this->conditionalExpressions,
3741+
$this->intersectConditionalExpressions($finallyScope->conditionalExpressions),
37423742
$this->inClosureBindScopeClasses,
37433743
$this->anonymousFunctionReflection,
37443744
$this->inFirstLevelStatement,
@@ -3883,7 +3883,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope
38833883
$this->getNamespace(),
38843884
$expressionTypes,
38853885
$nativeTypes,
3886-
$this->conditionalExpressions,
3886+
$this->intersectConditionalExpressions($finalScope->conditionalExpressions),
38873887
$this->inClosureBindScopeClasses,
38883888
$this->anonymousFunctionReflection,
38893889
$this->inFirstLevelStatement,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
8+
class Bug14446Test extends TypeInferenceTestCase
9+
{
10+
11+
public static function dataFileAsserts(): iterable
12+
{
13+
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-14446.php');
14+
}
15+
16+
/**
17+
* @param mixed ...$args
18+
*/
19+
#[DataProvider('dataFileAsserts')]
20+
public function testFileAsserts(
21+
string $assertType,
22+
string $file,
23+
...$args,
24+
): void
25+
{
26+
$this->assertFileAsserts($assertType, $file, ...$args);
27+
}
28+
29+
public static function getAdditionalConfigFiles(): array
30+
{
31+
return [
32+
__DIR__ . '/bug-14446.neon',
33+
];
34+
}
35+
36+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
parameters:
2+
polluteScopeWithAlwaysIterableForeach: false
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14446;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function test(bool $initial): void {
8+
$current = $initial;
9+
10+
while (true) {
11+
assertType('bool', $initial);
12+
if (!$current) {
13+
assertType('bool', $initial);
14+
break;
15+
}
16+
17+
$items = [1];
18+
foreach ($items as $item) {
19+
$current = false;
20+
}
21+
}
22+
23+
assertType('bool', $initial);
24+
var_dump($initial === true);
25+
}
26+
27+
function testMaybeIterable(bool $initial): void {
28+
$current = $initial;
29+
30+
while (true) {
31+
assertType('bool', $initial);
32+
if (!$current) {
33+
assertType('bool', $initial);
34+
break;
35+
}
36+
37+
$items = rand() > 0 ? [1] : [];
38+
foreach ($items as $item) {
39+
$current = false;
40+
}
41+
}
42+
43+
assertType('bool', $initial);
44+
var_dump($initial === true);
45+
}
46+
47+
function testFinally(bool $initial): void {
48+
$current = $initial;
49+
try {
50+
// nothing
51+
} finally {
52+
$current = false;
53+
}
54+
assertType('false', $current);
55+
assertType('bool', $initial);
56+
if (!$current) {
57+
assertType('bool', $initial);
58+
}
59+
}
60+
61+
function testFinallyWithCatch(bool $initial): void {
62+
$current = $initial;
63+
try {
64+
doSomething();
65+
} catch (\Exception $e) {
66+
// nothing
67+
} finally {
68+
$current = false;
69+
}
70+
assertType('false', $current);
71+
if (!$current) {
72+
assertType('bool', $initial);
73+
}
74+
assertType('bool', $initial);
75+
}
76+
77+
function doSomething(): void {}
78+
79+
/**
80+
* @param mixed $value
81+
*/
82+
function testForeachKeyOverwrite($value): void {
83+
if (is_array($value) && $value !== []) {
84+
$hasOnlyStringKey = true;
85+
foreach (array_keys($value) as $key) {
86+
if (is_int($key)) {
87+
$hasOnlyStringKey = false;
88+
break;
89+
}
90+
}
91+
92+
assertType('bool', $hasOnlyStringKey);
93+
94+
if ($hasOnlyStringKey) {
95+
// $key should not be in scope here with polluteScopeWithAlwaysIterableForeach: false
96+
// Second foreach should not report "Foreach overwrites $key with its key variable"
97+
foreach ($value as $key => $element) {
98+
assertType('(int|string)', $key);
99+
}
100+
}
101+
}
102+
}

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase
2020

2121
private bool $treatPhpDocTypesAsCertain = true;
2222

23+
private bool $polluteScopeWithAlwaysIterableForeach = true;
24+
2325
protected function getRule(): Rule
2426
{
2527
return new StrictComparisonOfDifferentTypesRule(
@@ -36,6 +38,11 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool
3638
return $this->treatPhpDocTypesAsCertain;
3739
}
3840

41+
protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool
42+
{
43+
return $this->polluteScopeWithAlwaysIterableForeach;
44+
}
45+
3946
public function testStrictComparison(): void
4047
{
4148
$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
@@ -1184,4 +1191,10 @@ public function testBug13421(): void
11841191
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []);
11851192
}
11861193

1194+
public function testBug14446(): void
1195+
{
1196+
$this->polluteScopeWithAlwaysIterableForeach = false;
1197+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []);
1198+
}
1199+
11871200
}

0 commit comments

Comments
 (0)