From 470212f3657816554ede3cb921fe838416a83e14 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 14 May 2026 08:00:57 +0000 Subject: [PATCH 1/5] Re-check scalar types after integer range expansion in TypeCombinator::union When an IntegerRangeType absorbs a ConstantIntegerType and expands (e.g., int<2,3> absorbs 1 to become int<1,3>), previously-checked scalar items that were disjoint with the old range but adjacent to the new range were never re-checked. This caused union(0|int<2,3>, 1) to produce 0|int<1,3> instead of int<0,3>. The incomplete union caused ExpressionTypeHolder::equalTypes() to return false during scope merging, creating spurious conditional expressions that incorrectly narrowed superglobal types in branches guarded by comparisons on a conditionally-assigned variable. Fix: restart the inner scalar loop when a range expands, so all remaining scalars are re-tested against the updated range. --- src/Type/TypeCombinator.php | 3 +- tests/PHPStan/Analyser/nsrt/bug-14610.php | 22 +++++++ .../PHPStan/Rules/Variables/IssetRuleTest.php | 7 +++ .../Rules/Variables/data/bug-14610.php | 63 +++++++++++++++++++ tests/PHPStan/Type/TypeCombinatorTest.php | 11 ++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14610.php create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14610.php diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index af60550c859..df18959ae24 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -353,8 +353,9 @@ public static function union(Type ...$types): Type [$a, $b] = $compareResult; if ($a !== null) { $types[$i] = $a; - array_splice($scalarTypeItems, $j--, 1); + array_splice($scalarTypeItems, $j, 1); $scalarTypeItemsCount--; + $j = -1; continue 1; } if ($b !== null) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14610.php b/tests/PHPStan/Analyser/nsrt/bug-14610.php new file mode 100644 index 00000000000..7efe7463b88 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14610.php @@ -0,0 +1,22 @@ +', $value); + + if ($value == 0) { + assertType('array', $_SESSION); + } +} diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index c1492e62901..d644b68736e 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -574,4 +574,11 @@ public function testBug14393(): void ]); } + public function testBug14610(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-14610.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14610.php b/tests/PHPStan/Rules/Variables/data/bug-14610.php new file mode 100644 index 00000000000..122cce32531 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14610.php @@ -0,0 +1,63 @@ +', + ], [ [ new MixedType(), From 99c79c20d86085236054ab7fd6ae11bd05a6be62 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 14 May 2026 08:54:42 +0000 Subject: [PATCH 2/5] Add test cases for array parameter to show superglobal-specific behavior The bug only affects superglobals because they are not tracked in expressionTypes by default (they use lazy default types via getVariableType()). This means during scope merging in createConditionalExpressions(), the parameter $a exists in both branches' expressionTypes and gets correctly filtered out, while $_SESSION only exists in the isset-true branch, causing it to become a spurious type guard. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14610.php | 18 ++++++++++++++++++ .../PHPStan/Rules/Variables/data/bug-14610.php | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14610.php b/tests/PHPStan/Analyser/nsrt/bug-14610.php index 7efe7463b88..26bfcb2910c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14610.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14610.php @@ -20,3 +20,21 @@ function test(): void assertType('array', $_SESSION); } } + +/** @param array $a */ +function testWithParam($a): void +{ + $value = 0; + + if (isset($a['test'])) { + $value = rand(0,3); + if ($value == 1) { + } + } + + assertType('int<0, 3>', $value); + + if ($value == 0) { + assertType('array', $a); + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-14610.php b/tests/PHPStan/Rules/Variables/data/bug-14610.php index 122cce32531..24e532b7d96 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14610.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14610.php @@ -61,3 +61,19 @@ function testWithDifferentKey(): void $result = isset($_SESSION['other']); } } + +/** @param array $a */ +function testWithParam($a): void +{ + $value = 0; + + if (isset($a['test'])) { + $value = rand(0,3); + if ($value == 1) { + } + } + + if ($value == 0) { + $result = isset($a['test']); + } +} From 628e531da376685220f75d079028d61a91085379 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 14 May 2026 09:05:39 +0000 Subject: [PATCH 3/5] Remove testWithParam and merge bug-14610 test files into single nsrt file Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14610.php | 38 +++++++-- .../PHPStan/Rules/Variables/IssetRuleTest.php | 2 +- .../Rules/Variables/data/bug-14610.php | 79 ------------------- 3 files changed, 34 insertions(+), 85 deletions(-) delete mode 100644 tests/PHPStan/Rules/Variables/data/bug-14610.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14610.php b/tests/PHPStan/Analyser/nsrt/bug-14610.php index 26bfcb2910c..3ad65a6e069 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14610.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14610.php @@ -18,23 +18,51 @@ function test(): void if ($value == 0) { assertType('array', $_SESSION); + $result = isset($_SESSION['test']); // should not be reported as always exists } } -/** @param array $a */ -function testWithParam($a): void +function testWithOtherSuperglobals(): void { $value = 0; - if (isset($a['test'])) { + if (isset($_GET['key'])) { $value = rand(0,3); if ($value == 1) { } } - assertType('int<0, 3>', $value); + if ($value == 0) { + $result = isset($_GET['key']); + } +} + +function testWithStrictComparison(): void +{ + $value = 0; + + if (isset($_SESSION['test'])) { + $value = rand(0,3); + if ($value === 1) { + } + } + + if ($value === 0) { + $result = isset($_SESSION['test']); + } +} + +function testWithDifferentKey(): void +{ + $value = 0; + + if (isset($_SESSION['test'])) { + $value = rand(0,3); + if ($value == 1) { + } + } if ($value == 0) { - assertType('array', $a); + $result = isset($_SESSION['other']); } } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index d644b68736e..ff2cf199417 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -578,7 +578,7 @@ public function testBug14610(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-14610.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14610.php'], []); } } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14610.php b/tests/PHPStan/Rules/Variables/data/bug-14610.php deleted file mode 100644 index 24e532b7d96..00000000000 --- a/tests/PHPStan/Rules/Variables/data/bug-14610.php +++ /dev/null @@ -1,79 +0,0 @@ - $a */ -function testWithParam($a): void -{ - $value = 0; - - if (isset($a['test'])) { - $value = rand(0,3); - if ($value == 1) { - } - } - - if ($value == 0) { - $result = isset($a['test']); - } -} From e65d5575c6f2ba9a6bd256c2198afc53f56be8fe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 14 May 2026 11:30:48 +0200 Subject: [PATCH 4/5] another test --- tests/PHPStan/Type/TypeCombinatorTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 9845e34f024..c90b7684149 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1653,6 +1653,17 @@ public static function dataUnion(): iterable IntegerRangeType::class, 'int<0, 3>', ], + [ + [ + new ConstantIntegerType(1), + new UnionType([ + new ConstantIntegerType(0), + IntegerRangeType::fromInterval(2, 3), + ]), + ], + IntegerRangeType::class, + 'int<0, 3>', + ], [ [ new MixedType(), From 81ad1bf89d4b9c26d0be45a8bf57f6e2114144d0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 14 May 2026 12:01:09 +0200 Subject: [PATCH 5/5] another test --- tests/PHPStan/Type/TypeCombinatorTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index c90b7684149..f325c638da4 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -1664,6 +1664,20 @@ public static function dataUnion(): iterable IntegerRangeType::class, 'int<0, 3>', ], + [ + [ + new UnionType([ + new ConstantIntegerType(0), + IntegerRangeType::fromInterval(2, 3), + ]), + new UnionType([ + new ConstantIntegerType(10), + new ConstantIntegerType(1), + ]), + ], + UnionType::class, + '10|int<0, 3>', + ], [ [ new MixedType(),