From 038222ba8dd2d63b1d60bc0c83ffe319d3e7195c Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:47:02 +0000 Subject: [PATCH 01/10] Support integer range keys in constant arrays - When setting a constant array offset with a finite union of constant string types or a finite IntegerRangeType, produce a union of constant arrays instead of degrading to a general array type - Added resolveFiniteScalarKeyTypes() helper on ConstantArrayType to extract constant key types from string unions and integer ranges - Limited expansion to CHUNK_FINITE_TYPES_LIMIT (5) keys to avoid combinatorial explosion in loop fixpoint analysis - Excluded integer constant unions (e.g., 0|1 from loops) to prevent regression in loop variable tracking - Updated constant-array-type-set.php test expectations for more precise results with int<0,4> range offsets --- CLAUDE.md | 4 + src/Type/Constant/ConstantArrayType.php | 92 +++++++++++++++++++ .../Analyser/nsrt/constant-array-type-set.php | 2 +- ...onstant-union-offset-on-constant-array.php | 50 ++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php diff --git a/CLAUDE.md b/CLAUDE.md index 993b10f193..5990d84828 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -412,6 +412,10 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines - `symfony/console` - CLI interface - `hoa/compiler` - Used for regex type parsing +### ConstantArrayType::setOffsetValueType union expansion for finite offsets + +When `ConstantArrayType::setOffsetValueType()` receives a union of constant string keys (e.g., `'a'|'b'`) or a finite `IntegerRangeType` (e.g., `int<1,5>`) and at least one key is new (not already in the array), it creates a union of constant arrays — one for each possible key — instead of degrading to a general `ArrayType`. This is controlled by `CHUNK_FINITE_TYPES_LIMIT` (5) to avoid combinatorial explosion in loops. Integer constant unions (e.g., `0|1` from loop fixpoint analysis) are excluded to prevent regression in loop analysis; only string constant unions and `IntegerRangeType` expansions are supported. The `resolveFiniteScalarKeyTypes()` helper method resolves the offset type to individual constant keys. + ### Ternary expression type narrowing in TypeSpecifier `TypeSpecifier::specifyTypesInCondition()` handles ternary expressions (`$cond ? $a : $b`) for type narrowing. In a truthy context (e.g., inside `assert()`), the ternary is semantically equivalent to `($cond && $a) || (!$cond && $b)` — meaning if the condition is true, the "if" branch must be truthy, and if false, the "else" branch must be truthy. The fix converts ternary expressions to this `BooleanOr(BooleanAnd(...), BooleanAnd(...))` form so the existing OR/AND narrowing logic handles both branches correctly. This enables `assert($cond ? $x instanceof A : $x instanceof B)` to narrow `$x` to `A|B`. The `AssertFunctionTypeSpecifyingExtension` calls `specifyTypesInCondition` with `TypeSpecifierContext::createTruthy()` context for the assert argument. diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 6cff7ef3af..d581dbc0b8 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -47,6 +47,7 @@ use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_keys; @@ -61,6 +62,7 @@ use function count; use function implode; use function in_array; +use function is_int; use function is_string; use function min; use function pow; @@ -699,6 +701,35 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { + if ($offsetType !== null) { + $scalarKeyTypes = $this->resolveFiniteScalarKeyTypes($offsetType); + if ($scalarKeyTypes !== null) { + $hasNewKey = false; + foreach ($scalarKeyTypes as $scalarKeyType) { + $existingKeyFound = false; + foreach ($this->keyTypes as $existingKeyType) { + if ($existingKeyType->getValue() === $scalarKeyType->getValue()) { + $existingKeyFound = true; + break; + } + } + if (!$existingKeyFound) { + $hasNewKey = true; + break; + } + } + + if ($hasNewKey) { + $arrayTypes = []; + foreach ($scalarKeyTypes as $scalarKeyType) { + $arrayTypes[] = $this->setOffsetValueType($scalarKeyType, $valueType, $unionValues); + } + + return TypeCombinator::union(...$arrayTypes); + } + } + } + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); $builder->setOffsetValueType($offsetType, $valueType); @@ -713,6 +744,67 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T return $builder->getArray(); } + /** + * @return list|null + */ + private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array + { + $offsetType = $offsetType->toArrayKey(); + + // Handle unions of constant string types (e.g. 'a'|'b') + $constantStrings = $offsetType->getConstantStrings(); + if (count($constantStrings) >= 2 && count($constantStrings) <= self::CHUNK_FINITE_TYPES_LIMIT) { + $result = []; + foreach ($constantStrings as $constantString) { + $arrayKeyType = $constantString->toArrayKey(); + $scalarValues = $arrayKeyType->getConstantScalarValues(); + if (count($scalarValues) !== 1) { + return null; + } + if (is_int($scalarValues[0])) { + $result[] = new ConstantIntegerType($scalarValues[0]); + } elseif (is_string($scalarValues[0])) { + $result[] = new ConstantStringType($scalarValues[0]); + } else { + return null; + } + } + return $result; + } + + // Handle integer range types (e.g. int<1,5>) + $integerRanges = TypeUtils::getIntegerRanges($offsetType); + if (count($integerRanges) > 0) { + $finiteScalarTypes = []; + foreach ($integerRanges as $integerRange) { + $finiteTypes = $integerRange->getFiniteTypes(); + if ($finiteTypes === []) { + return null; + } + + foreach ($finiteTypes as $finiteType) { + $finiteScalarTypes[] = $finiteType; + } + } + + if (count($finiteScalarTypes) < 2 || count($finiteScalarTypes) > self::CHUNK_FINITE_TYPES_LIMIT) { + return null; + } + + $result = []; + foreach ($finiteScalarTypes as $scalarType) { + $arrayKeyType = $scalarType->toArrayKey(); + if (!$arrayKeyType instanceof ConstantIntegerType) { + return null; + } + $result[] = $arrayKeyType; + } + return $result; + } + + return null; + } + public function unsetOffset(Type $offsetType): Type { $offsetType = $offsetType->toArrayKey(); diff --git a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php index 9ae0b88828..0f32789b28 100644 --- a/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php +++ b/tests/PHPStan/Analyser/nsrt/constant-array-type-set.php @@ -94,7 +94,7 @@ public function doBar5(int $offset): void { $a = [false, false, false]; $a[$offset] = true; - assertType('non-empty-array, bool>', $a); + assertType("array{0: false, 1: false, 2: false, 4: true}|array{false, false, false, true}|array{false, false, true}|array{false, true, false}|array{true, false, false}", $a); } public function doBar6(bool $offset): void diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php new file mode 100644 index 0000000000..24f129c72a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -0,0 +1,50 @@ + $intRange + */ + public function doBar(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a); + } + + /** + * @param array{foo: int} $a + * @param int<0, max> $intRange + */ + public function doInfiniteRange(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('non-empty-array<\'foo\'|int<0, max>, int>', $a); + } + + /** + * @param array{0: 'a', 1: 'b'} $a + * @param int<0,1> $intRange + */ + public function doExistingKeys(array $a, $intRange): void + { + $a[$intRange] = 'c'; + assertType("array{'a'|'c', 'b'|'c'}", $a); + } + +} From efbc5fd13a20eb59de84838ac36496a9c2ad4c2d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:54:28 +0000 Subject: [PATCH 02/10] Add regression test for #9907 Closes https://github.com/phpstan/phpstan/issues/9907 --- tests/PHPStan/Analyser/nsrt/bug-9907.php | 20 +++++++++++ .../Rules/Methods/ReturnTypeRuleTest.php | 5 +++ tests/PHPStan/Rules/Methods/data/bug-9907.php | 34 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9907.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9907.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9907.php b/tests/PHPStan/Analyser/nsrt/bug-9907.php new file mode 100644 index 0000000000..0985776490 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9907.php @@ -0,0 +1,20 @@ +analyse([__DIR__ . '/data/bug-9669.php'], []); } + public function testBug9907(): void + { + $this->analyse([__DIR__ . '/data/bug-9907.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9907.php b/tests/PHPStan/Rules/Methods/data/bug-9907.php new file mode 100644 index 0000000000..39086dbd70 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9907.php @@ -0,0 +1,34 @@ + + * } + */ + public function diffAddresses(array $address1, array $address2): array + { + $addressDifference = array_diff_assoc($address1, $address2); + $differenceDetails = []; + + foreach ($addressDifference as $name => $differenceValue) { + $differenceDetails[$name] = [ + 'change_to' => $differenceValue, + ]; + } + + if (!empty(count($differenceDetails))) { + $differenceDetails['variation_count'] = count($differenceDetails); + } + + return $differenceDetails; + } +} From a243073e94792170e7505f789fc8f69de7757296 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:55:01 +0000 Subject: [PATCH 03/10] Add regression test for #7978 Closes https://github.com/phpstan/phpstan/issues/7978 --- tests/PHPStan/Analyser/nsrt/bug-7978.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7978.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-7978.php b/tests/PHPStan/Analyser/nsrt/bug-7978.php new file mode 100644 index 0000000000..7ae24f8be0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7978.php @@ -0,0 +1,24 @@ + ['username', 'password'], + 'headers' => ['app_id', 'app_key'], + ]; + + public function doSomething(): void + { + foreach (self::FIELD_SETS as $type => $fields) { + $credentials = []; + foreach ($fields as $field) { + $credentials[$field] = 'fake'; + } + assertType("array{app_id?: 'fake', app_key?: 'fake', password?: 'fake', username?: 'fake'}", $credentials); + } + } +} From 5dc4e18dadcccf49147158b9a9d444f85a772a26 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:55:42 +0000 Subject: [PATCH 04/10] Add regression test for #2294 Closes https://github.com/phpstan/phpstan/issues/2294 --- tests/PHPStan/Analyser/nsrt/bug-2294.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-2294.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2294.php b/tests/PHPStan/Analyser/nsrt/bug-2294.php new file mode 100644 index 0000000000..a69571d6df --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2294.php @@ -0,0 +1,15 @@ + null, 'B' => null]; + + $entries2 = []; + foreach($entries as $key => $value) { + $entries2[$key] = ['a' => 1, 'b' => 2]; + } + assertType("array{A?: array{a: 1, b: 2}, B?: array{a: 1, b: 2}}", $entries2); +}; From a68b970b6f0c09fe9ceb4e0b87bc8af09f23a2aa Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:56:11 +0000 Subject: [PATCH 05/10] Add regression test for #13000 Closes https://github.com/phpstan/phpstan/issues/13000 --- tests/PHPStan/Analyser/nsrt/bug-13000.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13000.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13000.php b/tests/PHPStan/Analyser/nsrt/bug-13000.php new file mode 100644 index 0000000000..b0138a3d9b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13000.php @@ -0,0 +1,13 @@ + '1', 'b' => '2'] as $key => $val) { + $r[$key] = $val; + } + assertType("array{a?: '1'|'2', b?: '1'|'2'}", $r); +}; From aa336f668de0ed03d75ba521e9f867e1486df433 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 09:56:52 +0000 Subject: [PATCH 06/10] Add regression test for #13759 Closes https://github.com/phpstan/phpstan/issues/13759 --- tests/PHPStan/Analyser/nsrt/bug-13759.php | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13759.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13759.php b/tests/PHPStan/Analyser/nsrt/bug-13759.php new file mode 100644 index 0000000000..07482c181f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13759.php @@ -0,0 +1,32 @@ += 8.0 + +namespace Bug13759; + +use function PHPStan\Testing\assertType; + +class Test +{ + public function scenario(): void + { + $ints = []; + foreach (['a', 'b'] as $key) { + $ints[$key] = 1; + } + $ints['c'] = 1; + + assertType("array{a?: 1, b?: 1, c: 1}", $ints); + + foreach (['a'] as $key) { + $ints[$key] = $this->intToSomething($ints[$key]); + } + + assertType("array{a: float|string, b?: 1, c: 1}", $ints); + } + + /** + * @return string|float + */ + protected function intToSomething(int $int): string|float { + return mt_rand(1, 2) ? (string)$int : (float)$int; + } +} From 3e19a98fa1c1c2dcbc506590b7ba164d09c127f9 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:31:12 +0000 Subject: [PATCH 07/10] Address review comments - Remove unnecessary toArrayKey() calls in resolveFiniteScalarKeyTypes() - Use ConstantArrayTypeBuilder instead of recursion in setOffsetValueType() - Remove CLAUDE.md changes - Add test case for int<0, 5>|int<10, 15> union of integer ranges Co-authored-by: Markus Staab Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 ---- src/Type/Constant/ConstantArrayType.php | 12 ++++++------ .../set-constant-union-offset-on-constant-array.php | 10 ++++++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5990d84828..993b10f193 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -412,10 +412,6 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines - `symfony/console` - CLI interface - `hoa/compiler` - Used for regex type parsing -### ConstantArrayType::setOffsetValueType union expansion for finite offsets - -When `ConstantArrayType::setOffsetValueType()` receives a union of constant string keys (e.g., `'a'|'b'`) or a finite `IntegerRangeType` (e.g., `int<1,5>`) and at least one key is new (not already in the array), it creates a union of constant arrays — one for each possible key — instead of degrading to a general `ArrayType`. This is controlled by `CHUNK_FINITE_TYPES_LIMIT` (5) to avoid combinatorial explosion in loops. Integer constant unions (e.g., `0|1` from loop fixpoint analysis) are excluded to prevent regression in loop analysis; only string constant unions and `IntegerRangeType` expansions are supported. The `resolveFiniteScalarKeyTypes()` helper method resolves the offset type to individual constant keys. - ### Ternary expression type narrowing in TypeSpecifier `TypeSpecifier::specifyTypesInCondition()` handles ternary expressions (`$cond ? $a : $b`) for type narrowing. In a truthy context (e.g., inside `assert()`), the ternary is semantically equivalent to `($cond && $a) || (!$cond && $b)` — meaning if the condition is true, the "if" branch must be truthy, and if false, the "else" branch must be truthy. The fix converts ternary expressions to this `BooleanOr(BooleanAnd(...), BooleanAnd(...))` form so the existing OR/AND narrowing logic handles both branches correctly. This enables `assert($cond ? $x instanceof A : $x instanceof B)` to narrow `$x` to `A|B`. The `AssertFunctionTypeSpecifyingExtension` calls `specifyTypesInCondition` with `TypeSpecifierContext::createTruthy()` context for the assert argument. diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index d581dbc0b8..253823b33d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -722,7 +722,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni if ($hasNewKey) { $arrayTypes = []; foreach ($scalarKeyTypes as $scalarKeyType) { - $arrayTypes[] = $this->setOffsetValueType($scalarKeyType, $valueType, $unionValues); + $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $builder->setOffsetValueType($scalarKeyType, $valueType); + $arrayTypes[] = $builder->getArray(); } return TypeCombinator::union(...$arrayTypes); @@ -756,8 +758,7 @@ private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array if (count($constantStrings) >= 2 && count($constantStrings) <= self::CHUNK_FINITE_TYPES_LIMIT) { $result = []; foreach ($constantStrings as $constantString) { - $arrayKeyType = $constantString->toArrayKey(); - $scalarValues = $arrayKeyType->getConstantScalarValues(); + $scalarValues = $constantString->getConstantScalarValues(); if (count($scalarValues) !== 1) { return null; } @@ -793,11 +794,10 @@ private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array $result = []; foreach ($finiteScalarTypes as $scalarType) { - $arrayKeyType = $scalarType->toArrayKey(); - if (!$arrayKeyType instanceof ConstantIntegerType) { + if (!$scalarType instanceof ConstantIntegerType) { return null; } - $result[] = $arrayKeyType; + $result[] = $scalarType; } return $result; } diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php index 24f129c72a..07115d7944 100644 --- a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -37,6 +37,16 @@ public function doInfiniteRange(array $a, $intRange): void assertType('non-empty-array<\'foo\'|int<0, max>, int>', $a); } + /** + * @param array{foo: int} $a + * @param int<0, 5>|int<10, 15> $intRange + */ + public function doUnionOfRanges(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('non-empty-array<\'foo\'|int<0, 5>|int<10, 15>, int>', $a); + } + /** * @param array{0: 'a', 1: 'b'} $a * @param int<0,1> $intRange From 56c981c50c3835deb86eb17fb60d2fe6c6c65456 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 10:38:03 +0000 Subject: [PATCH 08/10] Add regression test for #14129 Closes https://github.com/phpstan/phpstan/issues/14129 --- tests/PHPStan/Analyser/nsrt/bug-14129.php | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14129.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14129.php b/tests/PHPStan/Analyser/nsrt/bug-14129.php new file mode 100644 index 0000000000..c044e0b0e6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14129.php @@ -0,0 +1,30 @@ + $intRange + */ + public function doBar(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a); + } + +} From a4d6589739b1dc828b2170b2988d148456206621 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 10:47:04 +0000 Subject: [PATCH 09/10] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- src/Type/Constant/ConstantArrayType.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 253823b33d..02647714de 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -792,14 +792,7 @@ private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array return null; } - $result = []; - foreach ($finiteScalarTypes as $scalarType) { - if (!$scalarType instanceof ConstantIntegerType) { - return null; - } - $result[] = $scalarType; - } - return $result; + return $finiteScalarTypes; } return null; From 4277198b6608d7f3dbb13a83b13abda979e326d0 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:35:54 +0000 Subject: [PATCH 10/10] Address review comments - Remove bug-14129.php (already covered by set-constant-union-offset-on-constant-array.php) - Move bug-7978.php from NSRT to CallMethodsRuleTest - Add de-duplication for overlapping integer ranges in resolveFiniteScalarKeyTypes() - Add test for overlapping ranges (int<0,3>|int<2,4>) Co-authored-by: Markus Staab --- src/Type/Constant/ConstantArrayType.php | 5 ++++ tests/PHPStan/Analyser/nsrt/bug-14129.php | 30 ------------------- ...onstant-union-offset-on-constant-array.php | 10 +++++++ .../Rules/Methods/CallMethodsRuleTest.php | 8 +++++ .../nsrt => Rules/Methods/data}/bug-7978.php | 11 +++++-- 5 files changed, 31 insertions(+), 33 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-14129.php rename tests/PHPStan/{Analyser/nsrt => Rules/Methods/data}/bug-7978.php (69%) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 02647714de..4b3ca1fcc5 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -777,6 +777,7 @@ private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array $integerRanges = TypeUtils::getIntegerRanges($offsetType); if (count($integerRanges) > 0) { $finiteScalarTypes = []; + $seen = []; foreach ($integerRanges as $integerRange) { $finiteTypes = $integerRange->getFiniteTypes(); if ($finiteTypes === []) { @@ -784,6 +785,10 @@ private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array } foreach ($finiteTypes as $finiteType) { + if (isset($seen[$finiteType->getValue()])) { + continue; + } + $seen[$finiteType->getValue()] = true; $finiteScalarTypes[] = $finiteType; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14129.php b/tests/PHPStan/Analyser/nsrt/bug-14129.php deleted file mode 100644 index c044e0b0e6..0000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-14129.php +++ /dev/null @@ -1,30 +0,0 @@ - $intRange - */ - public function doBar(array $a, $intRange): void - { - $a[$intRange] = 256; - assertType('array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a); - } - -} diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php index 07115d7944..d503ca7b40 100644 --- a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -47,6 +47,16 @@ public function doUnionOfRanges(array $a, $intRange): void assertType('non-empty-array<\'foo\'|int<0, 5>|int<10, 15>, int>', $a); } + /** + * @param array{foo: int} $a + * @param int<0, 3>|int<2, 4> $intRange + */ + public function doOverlappingRanges(array $a, $intRange): void + { + $a[$intRange] = 256; + assertType('array{foo: int, 0: 256}|array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}', $a); + } + /** * @param array{0: 'a', 1: 'b'} $a * @param int<0,1> $intRange diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 0ab14fd52a..2bb2c5c2e6 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3853,4 +3853,12 @@ public function testBug13805(): void $this->analyse([__DIR__ . '/data/bug-13805.php'], []); } + public function testBug7978(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-7978.php'], []); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7978.php b/tests/PHPStan/Rules/Methods/data/bug-7978.php similarity index 69% rename from tests/PHPStan/Analyser/nsrt/bug-7978.php rename to tests/PHPStan/Rules/Methods/data/bug-7978.php index 7ae24f8be0..4b3f951835 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7978.php +++ b/tests/PHPStan/Rules/Methods/data/bug-7978.php @@ -2,8 +2,6 @@ namespace Bug7978; -use function PHPStan\Testing\assertType; - class Test { const FIELD_SETS = [ @@ -11,6 +9,13 @@ class Test { 'headers' => ['app_id', 'app_key'], ]; + /** + * @param array $credentials + */ + public function acceptCredentials(array $credentials): void + { + } + public function doSomething(): void { foreach (self::FIELD_SETS as $type => $fields) { @@ -18,7 +23,7 @@ public function doSomething(): void foreach ($fields as $field) { $credentials[$field] = 'fake'; } - assertType("array{app_id?: 'fake', app_key?: 'fake', password?: 'fake', username?: 'fake'}", $credentials); + $this->acceptCredentials($credentials); } } }