Skip to content

Commit b0ee6d3

Browse files
staabmphpstan-bot
authored andcommitted
Fix phpstan/phpstan#12597: Variable might not be undefined after in_array check
When `in_array($type, [TYPE_1, TYPE_2], true)` narrows `$type` to a union like `1|2` and a variable is assigned inside that block, a conditional expression is created with guard type `1|2`. Later, when `$type === TYPE_1` narrows to just `1`, the conditional didn't match because `equals()` requires exact type equality. Fix: In `filterBySpecifiedTypes`, allow conditional expression guards to match when the specified type is a strict subtype of a finite union guard type. This enables `$type = 1` to match the guard `$type = 1|2`, correctly resolving the variable as defined. The subtype matching is restricted to: - Guards with >1 finite types (union of constants/literals) - Both guard and specified having YES certainty - The conditional's type holder having YES certainty (prevents matching stale "undefined" conditionals from earlier scope merges) - Matches don't cascade (not added to specifiedExpressions) to prevent mutual conditional loops from corrupting type information
1 parent fd42dc7 commit b0ee6d3

File tree

4 files changed

+59
-8
lines changed

4 files changed

+59
-8
lines changed

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3217,13 +3217,33 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
32173217
continue;
32183218
}
32193219
foreach ($conditionalExpressions as $conditionalExpression) {
3220+
$subtypeMatch = false;
32203221
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
3221-
if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) {
3222+
if (!array_key_exists($holderExprString, $specifiedExpressions)) {
32223223
continue 2;
32233224
}
3225+
$specifiedHolder = $specifiedExpressions[$holderExprString];
3226+
if ($specifiedHolder->equals($conditionalTypeHolder)) {
3227+
continue;
3228+
}
3229+
if (
3230+
$conditionalExpression->getTypeHolder()->getCertainty()->yes()
3231+
&& $specifiedHolder->getCertainty()->yes()
3232+
&& $conditionalTypeHolder->getCertainty()->yes()
3233+
&& count($conditionalTypeHolder->getType()->getFiniteTypes()) > 1
3234+
&& $conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes()
3235+
) {
3236+
$subtypeMatch = true;
3237+
continue;
3238+
}
3239+
continue 2;
32243240
}
32253241

32263242
$conditions[$conditionalExprString][] = $conditionalExpression;
3243+
if ($subtypeMatch) {
3244+
continue;
3245+
}
3246+
32273247
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
32283248
}
32293249
}

tests/PHPStan/Analyser/nsrt/bug-5051.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,35 +60,35 @@ public function testWithBooleans($data): void
6060
assertType('bool', $update);
6161
} else {
6262
assertType('1|2', $data);
63-
assertType('bool', $update);
63+
assertType('false', $update);
6464
}
6565

6666
if ($data === 1) {
67-
assertType('bool', $update);
68-
assertType('bool', $foo);
67+
assertType('false', $update);
68+
assertType('false', $foo);
6969
} else {
7070
assertType('bool', $update);
7171
assertType('bool', $foo);
7272
}
7373

7474
if ($data === 2) {
75-
assertType('bool', $update);
76-
assertType('bool', $foo);
75+
assertType('false', $update);
76+
assertType('false', $foo);
7777
} else {
7878
assertType('bool', $update);
7979
assertType('bool', $foo);
8080
}
8181

8282
if ($data === 3) {
83-
assertType('bool', $update);
83+
assertType('false', $update);
8484
assertType('true', $foo);
8585
} else {
8686
assertType('bool', $update);
8787
assertType('bool', $foo);
8888
}
8989

9090
if ($data === 1 || $data === 2) {
91-
assertType('bool', $update);
91+
assertType('false', $update);
9292
assertType('false', $foo);
9393
} else {
9494
assertType('bool', $update);

tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,15 @@ public function testBug14227(): void
14761476
$this->analyse([__DIR__ . '/data/bug-14227.php'], []);
14771477
}
14781478

1479+
public function testBug12597(): void
1480+
{
1481+
$this->cliArgumentsVariablesRegistered = true;
1482+
$this->polluteScopeWithLoopInitialAssignments = false;
1483+
$this->checkMaybeUndefinedVariables = true;
1484+
$this->polluteScopeWithAlwaysIterableForeach = true;
1485+
$this->analyse([__DIR__ . '/data/bug-12597.php'], []);
1486+
}
1487+
14791488
public function testBug14117(): void
14801489
{
14811490
$this->cliArgumentsVariablesRegistered = true;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12597;
4+
5+
class HelloWorld
6+
{
7+
private const TYPE_1 = 1;
8+
private const TYPE_2 = 2;
9+
10+
public function test(int $type): void
11+
{
12+
if (in_array($type, [self::TYPE_1, self::TYPE_2], true)) {
13+
$message = 'Hello!';
14+
}
15+
16+
if ($type === self::TYPE_1) {
17+
$this->message($message);
18+
}
19+
}
20+
21+
public function message(string $message): void {}
22+
}

0 commit comments

Comments
 (0)