From 69986aa2e54ca207a58a70c8ccdaff917def0c7c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:22:07 +0000 Subject: [PATCH] Fix false positive for numeric string offset on string-keyed array in isset() - In ArrayType::hasOffsetValueType and getOffsetValueType, toArrayKey() converts numeric strings like '1234' to integers, losing the original string type info - Added check against original offset type before toArrayKey() conversion so that a string offset on a string-keyed array is not incorrectly rejected - Updated NonexistentOffsetInArrayDimFetchRuleTest to reflect that '0' on array is no longer a false positive - New regression test in tests/PHPStan/Rules/Variables/data/bug-4296.php Closes https://github.com/phpstan/phpstan/issues/4296 --- src/Type/ArrayType.php | 4 +++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 --- .../PHPStan/Rules/Variables/IssetRuleTest.php | 7 +++++ .../PHPStan/Rules/Variables/data/bug-4296.php | 30 +++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-4296.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 288caefdc6..5754709130 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -268,6 +268,7 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType public function hasOffsetValueType(Type $offsetType): TrinaryLogic { + $originalOffsetType = $offsetType; $offsetArrayKeyType = $offsetType->toArrayKey(); if ($offsetArrayKeyType instanceof ErrorType) { $allowedArrayKeys = AllowedArrayKeysTypes::getType(); @@ -279,6 +280,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic $offsetType = $offsetArrayKeyType; if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && $this->getKeyType()->isSuperTypeOf($originalOffsetType)->no() && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) ) { return TrinaryLogic::createNo(); @@ -289,8 +291,10 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { + $originalOffsetType = $offsetType; $offsetType = $offsetType->toArrayKey(); if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && $this->getKeyType()->isSuperTypeOf($originalOffsetType)->no() && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) ) { return new ErrorType(); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index c066553f27..91d3d7ef02 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -63,10 +63,6 @@ public function testRule(): void 'Offset 0 does not exist on array.', 111, ], - [ - 'Offset \'0\' does not exist on array.', - 112, - ], [ 'Offset int does not exist on array.', 114, diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 045af8b58b..85dc192667 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -513,4 +513,11 @@ public function testBug9503(): void $this->analyse([__DIR__ . '/data/bug-9503.php'], []); } + public function testBug4296(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-4296.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-4296.php b/tests/PHPStan/Rules/Variables/data/bug-4296.php new file mode 100644 index 0000000000..dd81534e3a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-4296.php @@ -0,0 +1,30 @@ +id = $id; + } + + public function getId(): string + { + return $this->id; + } +} + +function (): void { + $map = []; + foreach ([new Test('1234')] as $test) { + $map[$test->getId()] = $test; + } + + foreach (['1234'] as $value) { + if (isset($map[$value])) { + $found = 1; + } + } +};