From cdaa47f6e332d064baf5ebfd97e428a5476b6bd6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 28 Sep 2025 23:36:31 +0200 Subject: [PATCH] Fix ArrayType::hasOffsetValueType --- src/Type/ArrayType.php | 3 ++ src/Type/Constant/ConstantArrayType.php | 3 ++ .../Levels/data/arrayOffsetAccess-3.json | 7 +++- .../Levels/data/arrayOffsetAccess-7.json | 25 +++++++++++++++ .../Levels/data/stringOffsetAccess-3.json | 5 +++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 +++ tests/PHPStan/Type/ArrayTypeTest.php | 32 +++++++++++++++++++ .../Type/Constant/ConstantArrayTypeTest.php | 26 +++++++++++++++ 8 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 9f87759790..643a535752 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -272,6 +272,9 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic if ($offsetArrayKeyType instanceof ErrorType) { $allowedArrayKeys = AllowedArrayKeysTypes::getType(); $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey(); + if ($offsetArrayKeyType instanceof NeverType) { + return TrinaryLogic::createNo(); + } } $offsetType = $offsetArrayKeyType; diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index b4e2ed6f36..0b16e3e898 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -586,6 +586,9 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic if ($offsetArrayKeyType instanceof ErrorType) { $allowedArrayKeys = AllowedArrayKeysTypes::getType(); $offsetArrayKeyType = TypeCombinator::intersect($allowedArrayKeys, $offsetType)->toArrayKey(); + if ($offsetArrayKeyType instanceof NeverType) { + return TrinaryLogic::createNo(); + } } return $this->recursiveHasOffsetValueType($offsetArrayKeyType); diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json index 3e23caff09..3d9ed2b7e4 100644 --- a/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json @@ -3,5 +3,10 @@ "message": "Invalid array key type DateTimeImmutable.", "line": 17, "ignorable": true + }, + { + "message": "Offset DateTimeImmutable does not exist on array.", + "line": 17, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json index 674057be06..3f715fbdc8 100644 --- a/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json @@ -1,24 +1,49 @@ [ + { + "message": "Offset int|object might not exist on array.", + "line": 19, + "ignorable": true + }, { "message": "Possibly invalid array key type int|object.", "line": 19, "ignorable": true }, + { + "message": "Offset object|null might not exist on array.", + "line": 20, + "ignorable": true + }, { "message": "Possibly invalid array key type object|null.", "line": 20, "ignorable": true }, + { + "message": "Offset DateTimeImmutable might not exist on array|ArrayAccess.", + "line": 26, + "ignorable": true + }, { "message": "Possibly invalid array key type DateTimeImmutable.", "line": 26, "ignorable": true }, + { + "message": "Offset int|object might not exist on array|ArrayAccess.", + "line": 28, + "ignorable": true + }, { "message": "Possibly invalid array key type int|object.", "line": 28, "ignorable": true }, + { + "message": "Offset object|null might not exist on array|ArrayAccess.", + "line": 29, + "ignorable": true + }, { "message": "Possibly invalid array key type object|null.", "line": 29, diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json index 2b54f201d4..f7f4afafdd 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-3.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-3.json @@ -18,5 +18,10 @@ "message": "Invalid array key type stdClass.", "line": 59, "ignorable": true + }, + { + "message": "Offset stdClass does not exist on array{baz: 21}|array{foo: 17, bar: 19}.", + "line": 59, + "ignorable": true } ] diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index e5d17aba80..0caf2f969f 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -949,6 +949,10 @@ public function testBugObject(): void 'Offset int|object does not exist on array{baz: 21}|array{foo: 17, bar: 19}.', 12, ], + [ + 'Offset object does not exist on array.', + 21, + ], ]); } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 9b7b2cfcaf..59ed255a09 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -269,4 +269,36 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ ); } + public static function dataHasOffsetValueType(): array + { + return [ + [ + new ArrayType(new BenevolentUnionType([ + new IntegerType(), + new StringType(), + ]), new IntegerType()), + new ArrayType(new BenevolentUnionType([ + new IntegerType(), + new StringType(), + ]), new IntegerType()), + TrinaryLogic::createNo(), + ], + ]; + } + + #[DataProvider('dataHasOffsetValueType')] + public function testHasOffsetValueType( + ArrayType $type, + Type $offsetType, + TrinaryLogic $expectedResult, + ): void + { + $actualResult = $type->hasOffsetValueType($offsetType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasOffsetValueType(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())), + ); + } + } diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 623c86e3ec..1c2b6fc410 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -1044,4 +1044,30 @@ public function testValuesArray(ConstantArrayType $type, ConstantArrayType $expe $this->assertSame($expectedType->getNextAutoIndexes(), $actualType->getNextAutoIndexes()); } + public static function dataHasOffsetValueType(): array + { + return [ + [ + new ConstantArrayType([new ConstantIntegerType(0)], [new ConstantStringType('a')]), + new ConstantArrayType([new ConstantIntegerType(0)], [new ConstantStringType('a')]), + TrinaryLogic::createNo(), + ], + ]; + } + + #[DataProvider('dataHasOffsetValueType')] + public function testHasOffsetValueType( + ConstantArrayType $type, + Type $offsetType, + TrinaryLogic $expectedResult, + ): void + { + $actualResult = $type->hasOffsetValueType($offsetType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> hasOffsetValueType(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())), + ); + } + }