diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index cf8f3f43c1..547e2f4fe4 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -64,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->isUndefinedExpressionAllowed($node) && !$isOffsetAccessible->no()) { + if ($scope->isUndefinedExpressionAllowed($node) && $isOffsetAccessibleType->isOffsetAccessLegal()->yes()) { return []; } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 43ebf895c2..6f76467aac 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -142,6 +142,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->getIterableKeyType()->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 3fd1b8a356..baf139b2dd 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -132,6 +132,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 771dec0e22..6ac04d9922 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -134,6 +134,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index cf4402f059..c261682b6a 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -134,6 +134,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index a15078542b..e2ebaa514a 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -137,6 +137,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 070f853329..98503a13a8 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -120,6 +120,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasMethod(%s)', $this->methodName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasMethod(string $methodName): TrinaryLogic { if ($this->getCanonicalMethodName() === strtolower($methodName)) { diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 2681b9ebb5..91a844d20c 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -138,6 +138,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index d230bbff48..8f07251305 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -146,6 +146,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) { diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index 53170faac3..f508447135 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -118,6 +118,11 @@ public function describe(VerbosityLevel $level): string return sprintf('hasProperty(%s)', $this->propertyName); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function hasProperty(string $propertyName): TrinaryLogic { if ($this->propertyName === $propertyName) { diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index cdd7d0b803..353dfe36e0 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -139,6 +139,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index dbab26f8d9..dc309f1a0e 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -135,6 +135,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 9d37ad7ee5..54dd4b36dd 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -393,6 +393,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $offsetType = $offsetType->toArrayKey(); diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index c0ac5b5920..aa8665f522 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -96,6 +96,11 @@ public function toArrayKey(): Type return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index c1cfe1a717..5986ed81fa 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -324,6 +324,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index d3498921e1..d637fdd24d 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -261,6 +261,11 @@ function (): string { ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isObject(): TrinaryLogic { return $this->objectType->isObject(); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index c82a50e4e7..eaf79b1794 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -144,6 +144,11 @@ public function toArrayKey(): Type return new IntegerType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 5e81e00519..0c57a8087a 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -96,6 +96,11 @@ public function toArrayKey(): Type return $this; } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index cc2a3f957c..f04fe05ef7 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -672,6 +672,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) { diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index c675aff51d..25c1ff3039 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -242,6 +242,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 14badc4786..78b80cd897 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -566,6 +566,16 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { + return TrinaryLogic::createYes(); + } + } + return TrinaryLogic::createMaybe(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isOffsetAccessible()->no()) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 2e9721f940..da6da83793 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -257,6 +257,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 5cf35ca527..b13f5d5105 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -152,6 +152,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 218b684bbb..469134dad3 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -174,6 +174,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 17bffaa7bb..a00324259a 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -428,6 +428,11 @@ public function describe(VerbosityLevel $level): string ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 6bba05fa01..4bfe6f4c62 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1099,6 +1099,11 @@ public function isOffsetAccessible(): TrinaryLogic ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->isOffsetAccessible(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index 51a3d07937..2d0cc64c1e 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -140,6 +140,11 @@ function () use ($level): string { ); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getEnumCases(): array { return []; diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 0eee8cea5f..afdcc5d432 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -86,6 +86,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index a9809f0362..df29b9eda2 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -375,6 +375,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->getStaticObjectType()->isOffsetAccessible(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->getStaticObjectType()->isOffsetAccessLegal(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->getStaticObjectType()->hasOffsetValueType($offsetType); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 2cb696d9f5..8bdd24093d 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -310,6 +310,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index c9972ee053..2090d52c85 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -56,6 +56,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 294726f2d9..df4d0c286b 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -222,6 +222,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->resolve()->isOffsetAccessible(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->resolve()->isOffsetAccessLegal(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->resolve()->hasOffsetValueType($offsetType); diff --git a/src/Type/Type.php b/src/Type/Type.php index c046c3048f..9c1f14fe5a 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -131,6 +131,8 @@ public function isList(): TrinaryLogic; public function isOffsetAccessible(): TrinaryLogic; + public function isOffsetAccessLegal(): TrinaryLogic; + public function hasOffsetValueType(Type $offsetType): TrinaryLogic; public function getOffsetValueType(Type $offsetType): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 791115abb2..b754092e81 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -647,6 +647,11 @@ public function isOffsetAccessible(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal()); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 07f755ce16..d0c9273d40 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -122,6 +122,11 @@ public function toArrayKey(): Type return new ErrorType(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 8ff137110b..23df32943a 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -28,5 +28,10 @@ "message": "Cannot access offset 'foo' on iterable.", "line": 58, "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on iterable.", + "line": 66, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 7cf2a42ef1..a833f3489a 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -115,6 +115,10 @@ public function testRule(): void 'Cannot access offset \'a\' on Closure(): void.', 253, ], + [ + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', + 258, + ], [ 'Offset null does not exist on array.', 310, @@ -252,6 +256,10 @@ public function testRuleBleedingEdge(): void 'Cannot access offset \'a\' on Closure(): void.', 253, ], + [ + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', + 258, + ], [ 'Offset null does not exist on array.', 310, @@ -670,6 +678,10 @@ public function testBug8068(): void "Cannot access offset 'path' on Closure.", 18, ], + [ + "Cannot access offset 'path' on iterable.", + 26, + ], ]); } @@ -737,6 +749,17 @@ public function testBug8166(): void ]); } + public function testBug10926(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-10926.php'], [ + [ + 'Cannot access offset \'a\' on stdClass.', + 10, + ], + ]); + } + public function testMixed(): void { $this->checkExplicitMixed = true; @@ -757,6 +780,40 @@ public function testMixed(): void ]); } + public function testOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal.php'], [ + [ + 'Cannot access offset 0 on Closure(): void.', + 7, + ], + [ + 'Cannot access offset 0 on stdClass.', + 12, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|stdClass.', + 96, + ], + [ + 'Cannot access offset 0 on array{\'test\'}|(Closure(): void).', + 98, + ], + ]); + } + + public function testNonExistentParentOffsetAccessLegal(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/offset-access-legal-non-existent-parent.php'], [ + [ + 'Cannot access offset 0 on parent.', + 9, + ], + ]); + } + public function dataReportPossiblyNonexistentArrayOffset(): iterable { yield [false, false, []]; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10926.php b/tests/PHPStan/Rules/Arrays/data/bug-10926.php new file mode 100644 index 0000000000..e8316112d5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10926.php @@ -0,0 +1,12 @@ += 8.0 + +namespace Bug10926; + +class HelloWorld +{ + public function sayHello(?\stdClass $date): void + { + $date ??= new \stdClass(); + echo isset($date['a']); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php new file mode 100644 index 0000000000..f5189b550f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php @@ -0,0 +1,11 @@ += 8.0 + +namespace OffsetAccessLegal; + +function closure(): void +{ + (function(){})[0] ?? "error"; +} + +function nonArrayAccessibleObject() +{ + (new \stdClass())[0] ?? "error"; +} + +function arrayAccessibleObject() +{ + (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + })[0] ?? "ok"; +} + +function array_(): void +{ + [0][0] ?? "ok"; +} + +function integer(): void +{ + (0)[0] ?? 'ok'; +} + +function float(): void +{ + (0.0)[0] ?? 'ok'; +} + +function null(): void +{ + (null)[0] ?? 'ok'; +} + +function bool(): void +{ + (true)[0] ?? 'ok'; +} + +function void(): void +{ + ((function (){})())[0] ?? 'ok'; +} + +function resource(): void +{ + (tmpfile())[0] ?? 'ok'; +} + +function offsetAccessibleMaybeAndLegal(): void +{ + $arrayAccessible = rand() ? (new class implements \ArrayAccess { + public function offsetExists($offset) { + return true; + } + + public function offsetGet($offset) { + return $offset; + } + + public function offsetSet($offset, $value) { + } + + public function offsetUnset($offset) { + } + }) : false; + + ($arrayAccessible)[0] ?? "ok"; + + (rand() ? "string" : true)[0] ?? "ok"; +} + +function offsetAccessibleMaybeAndIllegal(): void +{ + $arrayAccessible = rand() ? new \stdClass() : ['test']; + + ($arrayAccessible)[0] ?? "error"; + + (rand() ? function(){} : ['test'])[0] ?? "error"; +} diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index 6ee710a8c1..5fe7d0c409 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -1059,6 +1059,53 @@ public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $ty ); } + public function dataSubstractedIsOffsetLegal(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new ObjectType(ArrayAccess::class), + ]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectWithoutClassType(), + TrinaryLogic::createYes(), + ], + [ + new MixedType(), + new UnionType([ + new ObjectWithoutClassType(), + new StringType(), + ]), + TrinaryLogic::createYes(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsOffsetLegal + */ + public function testSubstractedIsOffsetLegal(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isOffsetAccessLegal(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessLegal()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + public function dataSubtractedHasOffsetValueType(): array { return [