From 64c084c306dd1bbf04c1714891c883786b3d4ff6 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:44:53 +0000 Subject: [PATCH 1/6] Fix false positive arrayValues.list for nested array with string keys - Fixed AccessoryArrayListType::setExistingOffsetValueType to return ErrorType when the offset type is definitely not an integer, dropping the list accessory - The root cause was that setExistingOffsetValueType unconditionally preserved the list type even when a string key was being set on the array - Added regression test for phpstan/phpstan#13629 (rule test + NSRT) --- src/Type/Accessory/AccessoryArrayListType.php | 4 ++ tests/PHPStan/Analyser/nsrt/bug-13629.php | 33 ++++++++++++++ .../Rules/Functions/ArrayValuesRuleTest.php | 5 +++ .../Rules/Functions/data/bug-13629.php | 45 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13629.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13629.php diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index ff3536f473..7bb3e88ddd 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -162,6 +162,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { + if ($offsetType->isInteger()->no()) { + return new ErrorType(); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13629.php b/tests/PHPStan/Analyser/nsrt/bug-13629.php new file mode 100644 index 0000000000..bb4e9fd5f9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13629.php @@ -0,0 +1,33 @@ +> $xsdFiles + * @param array> $groupedByNamespace + * @param array> $extraNamespaces + */ +function test(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces): void { + foreach ($extraNamespaces as $mergedNamespace) { + if (count($mergedNamespace) < 2) { + continue; + } + + $targetNamespace = end($mergedNamespace); + if (!isset($groupedByNamespace[$targetNamespace])) { + continue; + } + $xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace']; + + $xsdFiles[$xmlNamespace] = []; + foreach ($mergedNamespace as $namespace) { + foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { + $xsdFiles[$xmlNamespace][$viewHelper['name']] = $viewHelper; + } + } + // After assigning with string keys ($viewHelper['name']), $xsdFiles[$xmlNamespace] should NOT be a list + assertType('array, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]); + } +} diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php index 1f3e273b16..118db0305c 100644 --- a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -89,4 +89,9 @@ public function testFile(): void $this->analyse([__DIR__ . '/data/array_values_list.php'], $expectedErrors); } + public function testBug13629(): void + { + $this->analyse([__DIR__ . '/data/bug-13629.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13629.php b/tests/PHPStan/Rules/Functions/data/bug-13629.php new file mode 100644 index 0000000000..6678d9b695 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13629.php @@ -0,0 +1,45 @@ + 'http://typo3.org/ns/ns1', 'namespace' => 'TYPO3\Fluid', 'name' => 'Fluid'], + ['xmlNamespace' => 'http://typo3.org/ns/ns2', 'namespace' => 'TYPO3\Form', 'name' => 'Form'], + ['xmlNamespace' => 'http://typo3.org/ns/ns3', 'namespace' => 'TYPO3\Core', 'name' => 'Core'], + ['xmlNamespace' => 'http://typo3.org/ns/ns4', 'namespace' => 'Fluid\Fluid', 'name' => 'FluidCore'], +]; + +$extraNamespaces = [ + 'core' => ['TYPO3\Core'], + 'f' => ['TYPO3\Fluid', 'Fluid\Fluid'], + 'form' => ['TYPO3\Form'], +]; + +$xsdFiles = $groupedByNamespace = []; +foreach ($allViewHelpers as $viewHelper) { + $xsdFiles[$viewHelper['xmlNamespace']] ??= []; + $xsdFiles[$viewHelper['xmlNamespace']][] = $viewHelper; + + $groupedByNamespace[$viewHelper['namespace']] ??= []; + $groupedByNamespace[$viewHelper['namespace']][] = $viewHelper; +} + +foreach ($extraNamespaces as $mergedNamespace) { + if (count($mergedNamespace) < 2) { + continue; + } + + $targetNamespace = end($mergedNamespace); + if (!isset($groupedByNamespace[$targetNamespace])) { + continue; + } + $xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace']; + + $xsdFiles[$xmlNamespace] = []; + foreach ($mergedNamespace as $namespace) { + foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { + $xsdFiles[$xmlNamespace][$viewHelper['name']] = $viewHelper; + } + } + $xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]); +} From 3106d65336abe9fbe7029ce11077c44512c01634 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 21 Mar 2026 06:01:00 +0000 Subject: [PATCH 2/6] Remove duplicate test file, reuse NSRT test data for ArrayValuesRuleTest Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13629.php | 1 + .../Rules/Functions/ArrayValuesRuleTest.php | 2 +- .../Rules/Functions/data/bug-13629.php | 45 ------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 tests/PHPStan/Rules/Functions/data/bug-13629.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13629.php b/tests/PHPStan/Analyser/nsrt/bug-13629.php index bb4e9fd5f9..ed4d0b2a78 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13629.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13629.php @@ -29,5 +29,6 @@ function test(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces } // After assigning with string keys ($viewHelper['name']), $xsdFiles[$xmlNamespace] should NOT be a list assertType('array, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]); + $xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]); } } diff --git a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php index 118db0305c..37cb87b0f0 100644 --- a/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayValuesRuleTest.php @@ -91,7 +91,7 @@ public function testFile(): void public function testBug13629(): void { - $this->analyse([__DIR__ . '/data/bug-13629.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13629.php'], []); } } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13629.php b/tests/PHPStan/Rules/Functions/data/bug-13629.php deleted file mode 100644 index 6678d9b695..0000000000 --- a/tests/PHPStan/Rules/Functions/data/bug-13629.php +++ /dev/null @@ -1,45 +0,0 @@ - 'http://typo3.org/ns/ns1', 'namespace' => 'TYPO3\Fluid', 'name' => 'Fluid'], - ['xmlNamespace' => 'http://typo3.org/ns/ns2', 'namespace' => 'TYPO3\Form', 'name' => 'Form'], - ['xmlNamespace' => 'http://typo3.org/ns/ns3', 'namespace' => 'TYPO3\Core', 'name' => 'Core'], - ['xmlNamespace' => 'http://typo3.org/ns/ns4', 'namespace' => 'Fluid\Fluid', 'name' => 'FluidCore'], -]; - -$extraNamespaces = [ - 'core' => ['TYPO3\Core'], - 'f' => ['TYPO3\Fluid', 'Fluid\Fluid'], - 'form' => ['TYPO3\Form'], -]; - -$xsdFiles = $groupedByNamespace = []; -foreach ($allViewHelpers as $viewHelper) { - $xsdFiles[$viewHelper['xmlNamespace']] ??= []; - $xsdFiles[$viewHelper['xmlNamespace']][] = $viewHelper; - - $groupedByNamespace[$viewHelper['namespace']] ??= []; - $groupedByNamespace[$viewHelper['namespace']][] = $viewHelper; -} - -foreach ($extraNamespaces as $mergedNamespace) { - if (count($mergedNamespace) < 2) { - continue; - } - - $targetNamespace = end($mergedNamespace); - if (!isset($groupedByNamespace[$targetNamespace])) { - continue; - } - $xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace']; - - $xsdFiles[$xmlNamespace] = []; - foreach ($mergedNamespace as $namespace) { - foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { - $xsdFiles[$xmlNamespace][$viewHelper['name']] = $viewHelper; - } - } - $xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]); -} From b197c03eddd86fba30c7dce629ef1d901a4153da Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Mar 2026 08:20:37 +0100 Subject: [PATCH 3/6] more asserts --- tests/PHPStan/Analyser/nsrt/bug-13629.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13629.php b/tests/PHPStan/Analyser/nsrt/bug-13629.php index ed4d0b2a78..621b4169bb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13629.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13629.php @@ -21,6 +21,9 @@ function test(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces } $xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace']; + assertType('string', $xmlNamespace); + assertType('non-empty-list&hasOffsetValue(1, string)', $mergedNamespace); + $xsdFiles[$xmlNamespace] = []; foreach ($mergedNamespace as $namespace) { foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { From 3e363d2237d28a8c36692054b42e63a4a813193d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Mar 2026 09:39:01 +0100 Subject: [PATCH 4/6] Update AccessoryArrayListType.php --- src/Type/Accessory/AccessoryArrayListType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 7bb3e88ddd..6042d14ced 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -162,7 +162,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ($offsetType->isInteger()->no()) { + if ($this->hasOffsetValueType($offsetType)->no()) { return new ErrorType(); } From 298433c45d193ef54b791e94b09c2141dc953311 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Mar 2026 09:48:03 +0100 Subject: [PATCH 5/6] Update AccessoryArrayListType.php --- src/Type/Accessory/AccessoryArrayListType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 6042d14ced..a714555fda 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -162,7 +162,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ($this->hasOffsetValueType($offsetType)->no()) { + if ($this->getIterableKeyType()->isSuperTypeOf($offsetType)->no()) { return new ErrorType(); } From 2331102d3222f293af7ffc7f21c9ecca7d303cfc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Mar 2026 09:48:27 +0100 Subject: [PATCH 6/6] Revert "Update AccessoryArrayListType.php" This reverts commit 298433c45d193ef54b791e94b09c2141dc953311. --- src/Type/Accessory/AccessoryArrayListType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index a714555fda..6042d14ced 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -162,7 +162,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ($this->getIterableKeyType()->isSuperTypeOf($offsetType)->no()) { + if ($this->hasOffsetValueType($offsetType)->no()) { return new ErrorType(); }