From 8fc4759b4c07aa1ee1fd14f6676816e5708eb7f2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 27 Feb 2026 14:12:19 +0100 Subject: [PATCH] Fix list description with offset required --- src/Type/Constant/ConstantArrayType.php | 22 ++++++++++++++++--- tests/PHPStan/Analyser/nsrt/bug-14177.php | 6 ++--- .../PHPStan/Rules/Variables/IssetRuleTest.php | 16 ++++++++++++++ .../Variables/data/isset-constant-array.php | 22 +++++++++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/isset-constant-array.php diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 76c2f948f7..47d57f8b09 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -61,6 +61,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; @@ -1848,16 +1849,31 @@ public function makeOffsetRequired(Type $offsetType): self { $offsetType = $offsetType->toArrayKey(); $optionalKeys = $this->optionalKeys; + $isList = $this->isList->yes(); foreach ($this->keyTypes as $i => $keyType) { if (!$keyType->equals($offsetType)) { continue; } + $keyValue = $keyType->getValue(); foreach ($optionalKeys as $j => $key) { - if ($i === $key) { - unset($optionalKeys[$j]); - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); + if ( + $i !== $key + && ( + !$isList + || !is_int($keyValue) + || !is_int($this->keyTypes[$key]->getValue()) + || $this->keyTypes[$key]->getValue() >= $keyValue + ) + ) { + continue; } + + unset($optionalKeys[$j]); + } + + if (count($this->optionalKeys) !== count($optionalKeys)) { + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys), $this->isList); } break; diff --git a/tests/PHPStan/Analyser/nsrt/bug-14177.php b/tests/PHPStan/Analyser/nsrt/bug-14177.php index 53261d163e..26ef94e557 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14177.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14177.php @@ -12,7 +12,7 @@ class HelloWorld public function testList(array $b): void { if (array_key_exists(3, $b)) { - assertType('list{0: string, 1: string, 2?: string, 3: string}', $b); + assertType('array{string, string, string, string}', $b); } else { assertType('array{0: string, 1: string, 2?: string}', $b); } @@ -208,10 +208,10 @@ public function testFoo($l): void { if (array_key_exists(2, $l, true)) { assertType('true', array_is_list($l)); - assertType('list{0?: string, 1?: string, 2: string}', $l); + assertType('array{string, string, string}', $l); if (array_key_exists(1, $l, true)) { assertType('true', array_is_list($l)); - assertType('list{0?: string, 1: string, 2: string}', $l); + assertType('array{string, string, string}', $l); } else { assertType('true', array_is_list($l)); assertType('*NEVER*', $l); diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 045af8b58b..be2f2b3c68 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -499,6 +499,22 @@ public function testPr4374(): void ]); } + public function testIssetConstantArray(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/isset-constant-array.php'], [ + [ + 'Offset 2 on array{0: string, 1: string, 2: string, 3: string, 4?: string} in isset() always exists and is not nullable.', + 13, + ], + [ + 'Offset 3 on array{string, string, string, string, string} in isset() always exists and is not nullable.', + 17, + ], + ]); + } + public function testBug10640(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/data/isset-constant-array.php b/tests/PHPStan/Rules/Variables/data/isset-constant-array.php new file mode 100644 index 0000000000..44047da47a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/isset-constant-array.php @@ -0,0 +1,22 @@ +