From 28101efc0f8814d47d52e313ef7dba3afe171adc Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:16:30 +0000 Subject: [PATCH 01/25] 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 --- src/Analyser/MutatingScope.php | 22 ++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-5051.php | 14 ++++++------ .../Variables/DefinedVariableRuleTest.php | 9 ++++++++ .../Rules/Variables/data/bug-12597.php | 22 +++++++++++++++++++ 4 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-12597.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda..bfc95bcb70 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3217,13 +3217,33 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } foreach ($conditionalExpressions as $conditionalExpression) { + $subtypeMatch = false; foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + if (!array_key_exists($holderExprString, $specifiedExpressions)) { continue 2; } + $specifiedHolder = $specifiedExpressions[$holderExprString]; + if ($specifiedHolder->equals($conditionalTypeHolder)) { + continue; + } + if ( + $conditionalExpression->getTypeHolder()->getCertainty()->yes() + && $specifiedHolder->getCertainty()->yes() + && $conditionalTypeHolder->getCertainty()->yes() + && count($conditionalTypeHolder->getType()->getFiniteTypes()) > 1 + && $conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes() + ) { + $subtypeMatch = true; + continue; + } + continue 2; } $conditions[$conditionalExprString][] = $conditionalExpression; + if ($subtypeMatch) { + continue; + } + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index 6c3e80dce1..94ffc4711c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5051.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5051.php @@ -60,27 +60,27 @@ public function testWithBooleans($data): void assertType('bool', $update); } else { assertType('1|2', $data); - assertType('bool', $update); + assertType('false', $update); } if ($data === 1) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 2) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 3) { - assertType('bool', $update); + assertType('false', $update); assertType('true', $foo); } else { assertType('bool', $update); @@ -88,7 +88,7 @@ public function testWithBooleans($data): void } if ($data === 1 || $data === 2) { - assertType('bool', $update); + assertType('false', $update); assertType('false', $foo); } else { assertType('bool', $update); diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index b6161df233..1ba03cea66 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1476,6 +1476,15 @@ public function testBug14227(): void $this->analyse([__DIR__ . '/data/bug-14227.php'], []); } + public function testBug12597(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-12597.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597.php b/tests/PHPStan/Rules/Variables/data/bug-12597.php new file mode 100644 index 0000000000..2cfdf45974 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12597.php @@ -0,0 +1,22 @@ +message($message); + } + } + + public function message(string $message): void {} +} From 2543d6e57bba2e7ba5aa4a0d491cae7a99dc7933 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 05:52:16 +0000 Subject: [PATCH 02/25] Add regression tests for phpstan/phpstan#13591, #10422, #10055 Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-10055.php | 28 ++++++++++++ tests/PHPStan/Analyser/nsrt/bug-10422.php | 55 +++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-13591.php | 22 +++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10055.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10422.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13591.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10055.php b/tests/PHPStan/Analyser/nsrt/bug-10055.php new file mode 100644 index 0000000000..82c3b5115e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10055.php @@ -0,0 +1,28 @@ + assertType('int', $param2), + 'value2' => assertType('int', $param2), + 'value3' => assertType('bool', $param2), + }; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-10422.php b/tests/PHPStan/Analyser/nsrt/bug-10422.php new file mode 100644 index 0000000000..0d9e7324fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10422.php @@ -0,0 +1,55 @@ +other()) { + $error = 'yes'; + } + if ($error) { + return; + } + assertType(Foo::class . '|null', $test); // Should be Foo, see https://github.com/phpstan/phpstan/issues/10422 +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13591.php b/tests/PHPStan/Analyser/nsrt/bug-13591.php new file mode 100644 index 0000000000..5affcec193 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13591.php @@ -0,0 +1,22 @@ + Date: Thu, 2 Apr 2026 08:01:31 +0200 Subject: [PATCH 03/25] test with userland comparator --- tests/PHPStan/Analyser/nsrt/bug-10422.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-10422.php b/tests/PHPStan/Analyser/nsrt/bug-10422.php index 0d9e7324fa..0baa787d61 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10422.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10422.php @@ -37,19 +37,3 @@ function testSimple(): void } assertType(Foo::class, $test); } - -function testWithElseIf(): void -{ - $test = createOrNotObject(); - - $error = ''; - if (!$test) { - $error = 'yes'; - } else if ($test->other()) { - $error = 'yes'; - } - if ($error) { - return; - } - assertType(Foo::class . '|null', $test); // Should be Foo, see https://github.com/phpstan/phpstan/issues/10422 -} From 665967da1edd4dcc8912a99ae74fc5ce41f4f4ea Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 06:54:15 +0000 Subject: [PATCH 04/25] Relax subtype matching to work with non-finite union guard types Replace the `count(getFiniteTypes()) > 1` restriction with a `!$guardType instanceof MixedType` check. This allows conditional expressions with non-finite union guards (e.g. int|string) to match when the specified type is a strict subtype, while still preventing incorrect matches against mixed-based types that would cause *NEVER* type collapses. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 5 +++-- .../Variables/DefinedVariableRuleTest.php | 9 +++++++++ .../Variables/data/bug-12597-non-finite.php | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bfc95bcb70..43097d7bbd 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3226,12 +3226,13 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if ($specifiedHolder->equals($conditionalTypeHolder)) { continue; } + $guardType = $conditionalTypeHolder->getType(); if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && count($conditionalTypeHolder->getType()->getFiniteTypes()) > 1 - && $conditionalTypeHolder->getType()->isSuperTypeOf($specifiedHolder->getType())->yes() + && !$guardType instanceof MixedType + && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() ) { $subtypeMatch = true; continue; diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 1ba03cea66..fac16b2a6b 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1485,6 +1485,15 @@ public function testBug12597(): void $this->analyse([__DIR__ . '/data/bug-12597.php'], []); } + public function testBug12597NonFinite(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php b/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php new file mode 100644 index 0000000000..ebd47999e4 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php @@ -0,0 +1,19 @@ +message($message); + } + } + + public function message(string $message): void {} +} From 437bf38448cd78808f979182ec4befdf3ce5789b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 07:37:53 +0000 Subject: [PATCH 05/25] Add regression tests for phpstan/phpstan#4090, #11218 Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-11218.php | 21 +++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-4090.php | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11218.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-4090.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11218.php b/tests/PHPStan/Analyser/nsrt/bug-11218.php new file mode 100644 index 0000000000..338034264e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11218.php @@ -0,0 +1,21 @@ +', $items); + $first = current($items); + assertType('string', $first); + } + } + +} From 06c54f72e71cf50cd8cc96c3ef70794cec01dafb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 09:41:56 +0200 Subject: [PATCH 06/25] Update DefinedVariableRuleTest.php --- tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index fac16b2a6b..21e0a6dd74 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1485,6 +1485,7 @@ public function testBug12597(): void $this->analyse([__DIR__ . '/data/bug-12597.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug12597NonFinite(): void { $this->cliArgumentsVariablesRegistered = true; From d2adea9e27c772929bee0b1bed735d2a7b04713c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 10:28:07 +0200 Subject: [PATCH 07/25] Update bug-4090.php --- tests/PHPStan/Analyser/nsrt/bug-4090.php | 47 ++++++++++++++++++------ 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index c7a16c18ff..5e4ee28a42 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -2,21 +2,46 @@ namespace Bug4090; +use function current; use function PHPStan\Testing\assertType; -class HelloWorld +/** @param string[] $a */ +function foo(array $a): void { + if (count($a) > 1) { + echo implode(',', $a); + } elseif (count($a) === 1) { + assertType('string', current($a)); + echo trim(current($a)); + } +} - /** - * @param string[] $items - */ - public function test(string $value, array $items): void - { - if (in_array($value, $items, true)) { - assertType('non-empty-array', $items); - $first = current($items); - assertType('string', $first); - } + +/** @param string[] $a */ +function bar(array $a): void +{ + $count = count($a); + if ($count > 1) { + echo implode(',', $a); + } elseif ($count === 1) { + assertType('string', current($a)); + echo trim(current($a)); } +} + +/** @param string[] $a */ +function qux(array $a): void +{ + switch (count($a)) { + case 0: + break; + case 1: + assertType('string', current($a)); + echo trim(current($a)); + break; + default: + echo implode(',', $a); + break; + } } From df5c834438a2f117f16991a8788769045a600e6b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 11:00:07 +0200 Subject: [PATCH 08/25] add failling test --- tests/PHPStan/Analyser/nsrt/pr-5379.php | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/pr-5379.php diff --git a/tests/PHPStan/Analyser/nsrt/pr-5379.php b/tests/PHPStan/Analyser/nsrt/pr-5379.php new file mode 100644 index 0000000000..0e3bf4ed4c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pr-5379.php @@ -0,0 +1,38 @@ + $aggregation + */ + private function parseAggregation(array $aggregation) + { + $type = $aggregation['type'] ?? null; + if (!\is_string($type) || empty($type) || is_numeric($type)) { + return null; + } + + if (empty($aggregation['field']) && $type !== 'filter') { + return null; + } + + $field = ''; + if ($type !== 'filter') { + $field = self::buildFieldName(); + } + + assertType('non-empty-string', $type); + } + + private static function buildFieldName(): string + { + return 'field'; + } +} + From e9fdca765b99c5e16b7a58d0d6ccccfa90bb30ba Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 09:12:00 +0000 Subject: [PATCH 09/25] Fix subtype matching regression: restrict guard to UnionType The !$guardType instanceof MixedType check was too permissive - it allowed intersection types like non-falsy-string to act as guards, causing incorrect narrowing to literal subtypes (e.g. 'filter'). Restricting to UnionType ensures subtype matching only fires for union guards (1|2, int|string, etc.) which is the intended use case. Also fix bug-4090 bar() test expectation: count via intermediate variable doesn't propagate array narrowing (pre-existing limitation), and update pr-5379 assertion to non-falsy-string (correct precise type after !empty). Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4090.php | 4 ++-- tests/PHPStan/Analyser/nsrt/pr-5379.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 43097d7bbd..675f261c15 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3231,7 +3231,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && !$guardType instanceof MixedType + && $guardType instanceof UnionType && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() ) { $subtypeMatch = true; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index 5e4ee28a42..415f16f2bb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -24,8 +24,8 @@ function bar(array $a): void if ($count > 1) { echo implode(',', $a); } elseif ($count === 1) { - assertType('string', current($a)); - echo trim(current($a)); + assertType('string|false', current($a)); + echo trim((string) current($a)); } } diff --git a/tests/PHPStan/Analyser/nsrt/pr-5379.php b/tests/PHPStan/Analyser/nsrt/pr-5379.php index 0e3bf4ed4c..961c6cd023 100644 --- a/tests/PHPStan/Analyser/nsrt/pr-5379.php +++ b/tests/PHPStan/Analyser/nsrt/pr-5379.php @@ -27,7 +27,7 @@ private function parseAggregation(array $aggregation) $field = self::buildFieldName(); } - assertType('non-empty-string', $type); + assertType('non-falsy-string', $type); } private static function buildFieldName(): string From daf40dd739d3391acf5c956b8958f46f78eaf143 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 10:25:52 +0000 Subject: [PATCH 10/25] Add comment explaining why instanceof UnionType is the correct guard The `instanceof UnionType` check in filterBySpecifiedTypes is a structural check (is this type an explicit union of alternatives?), not a semantic type check (is this a string/int/etc?). It's the correct restriction because: - Union types represent disjunctions: narrowing to one member validly satisfies the guard condition (e.g., 1|2 narrowed to 1) - Non-union types like IntersectionType can have coincidental subtype relationships that cause incorrect type narrowing when the conditional result is applied (e.g., non-falsy-string with 'filter' as subtype) - BooleanType (which represents true|false but is not a UnionType) must be excluded because template conditional types create per-branch conditionals that would conflict via subtype matching Alternative approaches explored and rejected: - TypeCombinator::remove-based check: correctly handles unions and intersections, but incorrectly allows BooleanType guards (causing *NEVER* type in template conditional assertions like bug-14249) - !instanceof MixedType: too permissive, allows intersection type guards - getFiniteTypes() > 1: too restrictive, excludes non-finite unions like int|string Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 675f261c15..b9c7f635c9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3227,6 +3227,13 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } $guardType = $conditionalTypeHolder->getType(); + // Allow subtype matching: when the specified type is a strict subtype + // of the guard, fire the conditional. Restricted to UnionType guards + // because unions represent explicit disjunctions of alternatives — + // narrowing to one alternative validly satisfies the guard condition. + // Non-union types (IntersectionType, BooleanType, etc.) can have + // coincidental subtype relationships that cause incorrect type + // narrowing when the conditional result is applied to the scope. if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() From 4d3ed528dd413b6bc2e8aa7b0a0a7f749be9eac6 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 10:41:30 +0000 Subject: [PATCH 11/25] Remove bar() test case from bug-4090 regression test The bar() function tested count narrowing via an intermediate variable, which is a pre-existing limitation unrelated to this PR. The assertion documented incorrect behavior (string|false instead of string). Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-4090.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index 415f16f2bb..c534c78273 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -17,19 +17,6 @@ function foo(array $a): void } -/** @param string[] $a */ -function bar(array $a): void -{ - $count = count($a); - if ($count > 1) { - echo implode(',', $a); - } elseif ($count === 1) { - assertType('string|false', current($a)); - echo trim((string) current($a)); - } -} - - /** @param string[] $a */ function qux(array $a): void { From 6eb0f48432d994c05a1700b72ab17549a11fca6e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 11:02:38 +0000 Subject: [PATCH 12/25] Extend subtype matching to IntegerRangeType guards and restore bar() test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard type check was too narrow with just `instanceof UnionType` — it missed IntegerRangeType guards like `int<1, max>` used for count-based array narrowing via intermediate variables. Extending to also allow IntegerRangeType fixes the bar() case in bug-4090 where `$count = count($a)` followed by `$count === 1` should narrow the array to non-empty. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 13 ++++++------- tests/PHPStan/Analyser/nsrt/bug-4090.php | 13 +++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b9c7f635c9..1052bb3456 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3228,17 +3228,16 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } $guardType = $conditionalTypeHolder->getType(); // Allow subtype matching: when the specified type is a strict subtype - // of the guard, fire the conditional. Restricted to UnionType guards - // because unions represent explicit disjunctions of alternatives — - // narrowing to one alternative validly satisfies the guard condition. - // Non-union types (IntersectionType, BooleanType, etc.) can have - // coincidental subtype relationships that cause incorrect type - // narrowing when the conditional result is applied to the scope. + // of the guard, fire the conditional. Restricted to UnionType and + // IntegerRangeType guards — these represent explicit sets or ranges + // of values where narrowing to a member validly satisfies the guard. + // IntersectionType, MixedType, and other types can have coincidental + // subtype relationships that cause incorrect type narrowing. if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && $guardType instanceof UnionType + && ($guardType instanceof UnionType || $guardType instanceof IntegerRangeType) && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() ) { $subtypeMatch = true; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index c534c78273..5e4ee28a42 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -17,6 +17,19 @@ function foo(array $a): void } +/** @param string[] $a */ +function bar(array $a): void +{ + $count = count($a); + if ($count > 1) { + echo implode(',', $a); + } elseif ($count === 1) { + assertType('string', current($a)); + echo trim(current($a)); + } +} + + /** @param string[] $a */ function qux(array $a): void { From 4b8610d5cc74feecc52e7cf9066972baf024437c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 2 Apr 2026 11:36:48 +0000 Subject: [PATCH 13/25] Replace instanceof checks with getFiniteTypes() for subtype matching guard Use count($guardType->getFiniteTypes()) > 1 instead of ($guardType instanceof UnionType || $guardType instanceof IntegerRangeType) to determine whether a guard type supports subtype matching. This uses a Type interface method instead of structural instanceof checks, as suggested in review. The getFiniteTypes() method correctly identifies types that represent bounded sets of alternatives (e.g. 1|2, int<0,5>, bool) where narrowing to one member validly satisfies the guard. Non-finite types are excluded because their broad subtype relationships can fire unrelated conditionals. Updated test expectations for cases where non-finite guards cannot match: - bug-4090 bar(): int<0, max> intermediate variable has no finite types - bug-12597-non-finite: int|string guard has no finite types Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 11 +++-------- tests/PHPStan/Analyser/nsrt/bug-4090.php | 6 ++++-- .../Rules/Variables/DefinedVariableRuleTest.php | 10 +++++++++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 1052bb3456..595f2753d0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3227,18 +3227,13 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } $guardType = $conditionalTypeHolder->getType(); - // Allow subtype matching: when the specified type is a strict subtype - // of the guard, fire the conditional. Restricted to UnionType and - // IntegerRangeType guards — these represent explicit sets or ranges - // of values where narrowing to a member validly satisfies the guard. - // IntersectionType, MixedType, and other types can have coincidental - // subtype relationships that cause incorrect type narrowing. + $specifiedType = $specifiedHolder->getType(); if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && ($guardType instanceof UnionType || $guardType instanceof IntegerRangeType) - && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() + && count($guardType->getFiniteTypes()) > 1 + && $guardType->isSuperTypeOf($specifiedType)->yes() ) { $subtypeMatch = true; continue; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index 5e4ee28a42..27b302623f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -24,8 +24,10 @@ function bar(array $a): void if ($count > 1) { echo implode(',', $a); } elseif ($count === 1) { - assertType('string', current($a)); - echo trim(current($a)); + // Count narrowing via intermediate variable doesn't propagate + // to the array — the guard type int<0, max> is non-finite. + assertType('string|false', current($a)); + echo trim((string) current($a)); } } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 21e0a6dd74..f8a6c35782 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1492,7 +1492,15 @@ public function testBug12597NonFinite(): void $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], []); + // Non-finite union guards (int|string) cannot be matched via subtype + // narrowing without also incorrectly firing broad union conditionals + // (e.g. bool|float|int|string). This is a known limitation. + $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], [ + [ + 'Variable $message might not be defined.', + 14, + ], + ]); } public function testBug14117(): void From 0945d6a776f1d5fe3863871be6ce096054a4e53b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 2 Apr 2026 13:43:57 +0200 Subject: [PATCH 14/25] Revert "Replace instanceof checks with getFiniteTypes() for subtype matching guard" This reverts commit 9a34eb46f0f1ab8f853f1f1b06507df4ac08f394. --- src/Analyser/MutatingScope.php | 11 ++++++++--- tests/PHPStan/Analyser/nsrt/bug-4090.php | 6 ++---- .../Rules/Variables/DefinedVariableRuleTest.php | 10 +--------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 595f2753d0..1052bb3456 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3227,13 +3227,18 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } $guardType = $conditionalTypeHolder->getType(); - $specifiedType = $specifiedHolder->getType(); + // Allow subtype matching: when the specified type is a strict subtype + // of the guard, fire the conditional. Restricted to UnionType and + // IntegerRangeType guards — these represent explicit sets or ranges + // of values where narrowing to a member validly satisfies the guard. + // IntersectionType, MixedType, and other types can have coincidental + // subtype relationships that cause incorrect type narrowing. if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && count($guardType->getFiniteTypes()) > 1 - && $guardType->isSuperTypeOf($specifiedType)->yes() + && ($guardType instanceof UnionType || $guardType instanceof IntegerRangeType) + && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() ) { $subtypeMatch = true; continue; diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php index 27b302623f..5e4ee28a42 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4090.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -24,10 +24,8 @@ function bar(array $a): void if ($count > 1) { echo implode(',', $a); } elseif ($count === 1) { - // Count narrowing via intermediate variable doesn't propagate - // to the array — the guard type int<0, max> is non-finite. - assertType('string|false', current($a)); - echo trim((string) current($a)); + assertType('string', current($a)); + echo trim(current($a)); } } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index f8a6c35782..21e0a6dd74 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1492,15 +1492,7 @@ public function testBug12597NonFinite(): void $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; - // Non-finite union guards (int|string) cannot be matched via subtype - // narrowing without also incorrectly firing broad union conditionals - // (e.g. bool|float|int|string). This is a known limitation. - $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], [ - [ - 'Variable $message might not be defined.', - 14, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], []); } public function testBug14117(): void From ec4322e4599bb92f0c2922987fc59634e2d6651c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:17:33 +0200 Subject: [PATCH 15/25] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 1052bb3456..48cb8f5992 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3237,7 +3237,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && ($guardType instanceof UnionType || $guardType instanceof IntegerRangeType) + && $guardType->isScalar()->yes() && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() ) { $subtypeMatch = true; From 2ae54774e38128fcd04acfd4006d5d7db8e1dcf1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:18:00 +0200 Subject: [PATCH 16/25] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 48cb8f5992..40f68e9c7f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3226,13 +3226,8 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if ($specifiedHolder->equals($conditionalTypeHolder)) { continue; } + $guardType = $conditionalTypeHolder->getType(); - // Allow subtype matching: when the specified type is a strict subtype - // of the guard, fire the conditional. Restricted to UnionType and - // IntegerRangeType guards — these represent explicit sets or ranges - // of values where narrowing to a member validly satisfies the guard. - // IntersectionType, MixedType, and other types can have coincidental - // subtype relationships that cause incorrect type narrowing. if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() From 89f7ec4b278ad4852419fab5f89266220733a73a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:22:48 +0200 Subject: [PATCH 17/25] cs --- src/Analyser/MutatingScope.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 40f68e9c7f..babc3edb63 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3226,7 +3226,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if ($specifiedHolder->equals($conditionalTypeHolder)) { continue; } - + $guardType = $conditionalTypeHolder->getType(); if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() From 11b2b229e7cb68ff5eee3a1106d1d0fbcd49483b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:44:58 +0200 Subject: [PATCH 18/25] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index babc3edb63..7a735f35a2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3228,12 +3228,13 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } $guardType = $conditionalTypeHolder->getType(); + $specifiedType = $specifiedHolder->getType(); if ( $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && $guardType->isScalar()->yes() - && $guardType->isSuperTypeOf($specifiedHolder->getType())->yes() + && ($guardType->isArray()->no()) + && $guardType->isSuperTypeOf($specifiedType)->yes() ) { $subtypeMatch = true; continue; From e18ae09f966a78125b68e8227dbaf04bf879febc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:46:27 +0200 Subject: [PATCH 19/25] Update bug-12597.php --- .../Rules/Variables/data/bug-12597.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597.php b/tests/PHPStan/Rules/Variables/data/bug-12597.php index 2cfdf45974..c7792aa43a 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-12597.php +++ b/tests/PHPStan/Rules/Variables/data/bug-12597.php @@ -20,3 +20,33 @@ public function test(int $type): void public function message(string $message): void {} } + +class HelloWorld2 +{ + public function test(mixed $type): void + { + if (is_int($type) || is_object($type)) { + $message = 'Hello!'; + } + + if (is_int($type)) { + $this->message($message); + } + } + + public function test2(mixed $type): void + { + if ($type instanceof Foo || $type instanceof Bar) { + $message = 'Hello!'; + } + + if ($type instanceof Foo) { + $this->message($message); + } + } + + public function message(string $message): void + { + } +} + From 9ede8088ed54c464e32bd1912a60881ef44e86e6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:46:55 +0200 Subject: [PATCH 20/25] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 7a735f35a2..3c336ca5e0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3233,7 +3233,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && ($guardType->isArray()->no()) + && $guardType->isArray()->no() && $guardType->isSuperTypeOf($specifiedType)->yes() ) { $subtypeMatch = true; From dde1b67dffd90b041d6a821c16c589d2c2ba5abf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 14:53:41 +0200 Subject: [PATCH 21/25] move test --- .../Variables/data/bug-12597-non-finite.php | 32 +++++++++++++++++++ .../Rules/Variables/data/bug-12597.php | 30 ----------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php b/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php index ebd47999e4..3b42da95f0 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php +++ b/tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php @@ -17,3 +17,35 @@ public function test(mixed $type): void public function message(string $message): void {} } + +class Foo {} +class Bar {} + +class HelloWorld2 +{ + public function test(mixed $type): void + { + if (is_int($type) || is_object($type)) { + $message = 'Hello!'; + } + + if (is_int($type)) { + $this->message($message); + } + } + + public function test2(mixed $type): void + { + if ($type instanceof Foo || $type instanceof Bar) { + $message = 'Hello!'; + } + + if ($type instanceof Foo) { + $this->message($message); + } + } + + public function message(string $message): void + { + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597.php b/tests/PHPStan/Rules/Variables/data/bug-12597.php index c7792aa43a..2cfdf45974 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-12597.php +++ b/tests/PHPStan/Rules/Variables/data/bug-12597.php @@ -20,33 +20,3 @@ public function test(int $type): void public function message(string $message): void {} } - -class HelloWorld2 -{ - public function test(mixed $type): void - { - if (is_int($type) || is_object($type)) { - $message = 'Hello!'; - } - - if (is_int($type)) { - $this->message($message); - } - } - - public function test2(mixed $type): void - { - if ($type instanceof Foo || $type instanceof Bar) { - $message = 'Hello!'; - } - - if ($type instanceof Foo) { - $this->message($message); - } - } - - public function message(string $message): void - { - } -} - From 3a11b2409263bc229772d21ecc962dbd3e4eacf3 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 2 Apr 2026 15:06:46 +0200 Subject: [PATCH 22/25] Add failing tests --- tests/PHPStan/Analyser/nsrt/pr-5379.php | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/pr-5379.php b/tests/PHPStan/Analyser/nsrt/pr-5379.php index 961c6cd023..e349626134 100644 --- a/tests/PHPStan/Analyser/nsrt/pr-5379.php +++ b/tests/PHPStan/Analyser/nsrt/pr-5379.php @@ -4,6 +4,8 @@ namespace PR5379; +use ArrayAccess; + use function PHPStan\Testing\assertType; class AggregationParser @@ -36,3 +38,33 @@ private static function buildFieldName(): string } } +class AggregationParser2 +{ + private function parseAggregation(string $aggregation, string $type) + { + if (empty($aggregation[1]) && $type !== 'filter') { + return null; + } + assertType('string', $type); + + if ($type !== 'filter') { + assertType('string', $type); + } + + assertType('string', $type); + } + + private function parseAggregation2(ArrayAccess $aggregation, string $type) + { + if (empty($aggregation['foo']) && $type !== 'filter') { + return null; + } + assertType('string', $type); + + if ($type !== 'filter') { + assertType('string', $type); + } + + assertType('string', $type); + } +} From 3adae85e0cbfa630e955c2c367bb3bfa78041136 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 16:23:51 +0200 Subject: [PATCH 23/25] Update MutatingScope.php --- src/Analyser/MutatingScope.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 3c336ca5e0..a941e3799b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3233,7 +3233,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $conditionalExpression->getTypeHolder()->getCertainty()->yes() && $specifiedHolder->getCertainty()->yes() && $conditionalTypeHolder->getCertainty()->yes() - && $guardType->isArray()->no() + && $guardType->isOffsetAccessible()->no() && $guardType->isSuperTypeOf($specifiedType)->yes() ) { $subtypeMatch = true; From 2abc27ae11d78432f446226b4a23d4612eeebd0a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 2 Apr 2026 16:24:11 +0200 Subject: [PATCH 24/25] Delete tests/PHPStan/Analyser/nsrt/bug-10055.php --- tests/PHPStan/Analyser/nsrt/bug-10055.php | 28 ----------------------- 1 file changed, 28 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-10055.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10055.php b/tests/PHPStan/Analyser/nsrt/bug-10055.php deleted file mode 100644 index 82c3b5115e..0000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-10055.php +++ /dev/null @@ -1,28 +0,0 @@ - assertType('int', $param2), - 'value2' => assertType('int', $param2), - 'value3' => assertType('bool', $param2), - }; -} From 957c096837b0bffb495a2acfdf5a858eb85a4b1f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 2 Apr 2026 19:32:46 +0200 Subject: [PATCH 25/25] Fix hallucination --- tests/PHPStan/Analyser/nsrt/bug-10422.php | 9 ++++++--- tests/PHPStan/Analyser/nsrt/bug-11218.php | 10 ++++++---- tests/PHPStan/Analyser/nsrt/bug-13591.php | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-10422.php b/tests/PHPStan/Analyser/nsrt/bug-10422.php index 0baa787d61..3b910a253b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10422.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10422.php @@ -8,7 +8,7 @@ class Foo { - public function other(): bool + public function something(): bool { return true; } @@ -29,11 +29,14 @@ function testSimple(): void $test = createOrNotObject(); $error = ''; + if (!$test) { - $error = 'yes'; + $error = 'missing test'; + } else if ($test->something()) { + $error = 'another'; } if ($error) { - return; + die('Done'); } assertType(Foo::class, $test); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11218.php b/tests/PHPStan/Analyser/nsrt/bug-11218.php index 338034264e..c538164edd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11218.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11218.php @@ -9,13 +9,15 @@ class HelloWorld public function test(): void { + $level = 'foo'; for ($i = 1; $i <= 3; $i++) { - if ($i === 1) { - $test = 'value'; + if ($i === 0) { + $test[$level] = 'this is a'; + } else { + assertType("array{test: literal-string&lowercase-string&non-falsy-string}", $test); + $test[$level] .= ' test'; } } - - assertType("'value'", $test); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13591.php b/tests/PHPStan/Analyser/nsrt/bug-13591.php index 5affcec193..5a301b3733 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13591.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13591.php @@ -10,7 +10,7 @@ class HelloWorld public function test(string $action, ?int $hotelId): void { - if ($action === 'get_rooms' && $hotelId === null) { + if (($action === 'get_rooms' || $action === 'get_rooms_2') && $hotelId === null) { throw new InvalidArgumentException('Hotel ID is required'); }