From 5277d55fd0e6c8b1c7f9df06ba12748ccafa0ac8 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Sun, 5 May 2024 11:21:25 +0900 Subject: [PATCH 01/12] feat: implement isOffsetAccessLegal --- src/Type/Accessory/AccessoryArrayListType.php | 5 +++++ src/Type/Accessory/AccessoryLiteralStringType.php | 5 +++++ .../Accessory/AccessoryNonEmptyStringType.php | 5 +++++ .../Accessory/AccessoryNonFalsyStringType.php | 5 +++++ src/Type/Accessory/AccessoryNumericStringType.php | 5 +++++ src/Type/Accessory/HasOffsetType.php | 5 +++++ src/Type/Accessory/HasOffsetValueType.php | 5 +++++ src/Type/Accessory/NonEmptyArrayType.php | 5 +++++ src/Type/Accessory/OversizedArrayType.php | 5 +++++ src/Type/ArrayType.php | 5 +++++ src/Type/ClosureType.php | 5 +++++ src/Type/IntersectionType.php | 5 +++++ src/Type/MixedType.php | 15 +++++++++++++++ src/Type/NeverType.php | 5 +++++ src/Type/NullType.php | 5 +++++ src/Type/ObjectType.php | 5 +++++ src/Type/StaticType.php | 5 +++++ src/Type/StrictMixedType.php | 5 +++++ src/Type/StringType.php | 5 +++++ src/Type/Traits/LateResolvableTypeTrait.php | 5 +++++ .../Traits/MaybeOffsetAccessibleTypeTrait.php | 5 +++++ src/Type/Traits/NonOffsetAccessibleTypeTrait.php | 5 +++++ src/Type/Type.php | 2 ++ src/Type/UnionType.php | 5 +++++ 24 files changed, 127 insertions(+) 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/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/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/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/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/MixedType.php b/src/Type/MixedType.php index 14badc4786..0f39252001 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -566,6 +566,21 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $offsetAccessibles = new UnionType([ + new ClosureType(), + new ObjectWithoutClassType(), + ]); + + if ($this->subtractedType->isSuperTypeOf($offsetAccessibles)->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/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/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/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/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 6e83395e95..485d62566f 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -14,6 +14,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index d74dc8d0d1..92e0a8c29b 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -14,6 +14,11 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); 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)); From b81633f044a9ab001bb00e988e683fd57cbc27c4 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Sun, 5 May 2024 11:46:45 +0900 Subject: [PATCH 02/12] feat: use isOffsetAccessLegal in NonexistentOffsetInArrayDimFetchRule --- src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php | 2 +- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 7cf2a42ef1..2c264533e1 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, From 461cb89a955f90db4583053a91e5a8069f0688e7 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Sun, 5 May 2024 11:49:15 +0900 Subject: [PATCH 03/12] test: add test for issue-10926 --- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 11 +++++++++++ tests/PHPStan/Rules/Arrays/data/bug-10926.php | 12 ++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-10926.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2c264533e1..495429cac9 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -745,6 +745,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; 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']); + } +} From ed98710036e6d016c6113d8d410bb3c261f12143 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Sun, 5 May 2024 12:42:29 +0900 Subject: [PATCH 04/12] test: add test for offset access legal --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 23 +++++ .../Rules/Arrays/data/offset-access-legal.php | 99 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/offset-access-legal.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 495429cac9..4cfba0a70d 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -776,6 +776,29 @@ 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 dataReportPossiblyNonexistentArrayOffset(): iterable { yield [false, false, []]; diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php new file mode 100644 index 0000000000..fc0a35a929 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php @@ -0,0 +1,99 @@ += 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] ?? "error"; +} + +function array_(): void +{ + [0][0] ?? "error"; +} + +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] ?? "error"; + + (rand() ? "string" : true)[0] ?? "error"; +} + +function offsetAccessibleMaybeAndIllegal(): void +{ + $arrayAccessible = rand() ? new \stdClass() : ['test']; + + ($arrayAccessible)[0] ?? "error"; + + (rand() ? function(){} : ['test'])[0] ?? "error"; +} From b012509171139ba34f0afc83c540509ed991a2ef Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Sun, 5 May 2024 13:24:46 +0900 Subject: [PATCH 05/12] fix: fix isOffsetAccessLegal for mixed type --- src/Type/MixedType.php | 7 +---- tests/PHPStan/Type/MixedTypeTest.php | 47 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 0f39252001..78b80cd897 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -569,12 +569,7 @@ public function isOffsetAccessible(): TrinaryLogic public function isOffsetAccessLegal(): TrinaryLogic { if ($this->subtractedType !== null) { - $offsetAccessibles = new UnionType([ - new ClosureType(), - new ObjectWithoutClassType(), - ]); - - if ($this->subtractedType->isSuperTypeOf($offsetAccessibles)->yes()) { + if ($this->subtractedType->isSuperTypeOf(new ObjectWithoutClassType())->yes()) { return TrinaryLogic::createYes(); } } 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 [ From fbf47dd55a906844d667fb88af5afa49c5509c4f Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 09:58:01 +0900 Subject: [PATCH 06/12] chore: fix miss-leading comments --- tests/PHPStan/Rules/Arrays/data/offset-access-legal.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php index fc0a35a929..7d67dea8d2 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php @@ -28,12 +28,12 @@ public function offsetSet($offset, $value) { public function offsetUnset($offset) { } - })[0] ?? "error"; + })[0] ?? "ok"; } function array_(): void { - [0][0] ?? "error"; + [0][0] ?? "ok"; } function integer(): void @@ -84,9 +84,9 @@ public function offsetUnset($offset) { } }) : false; - ($arrayAccessible)[0] ?? "error"; + ($arrayAccessible)[0] ?? "ok"; - (rand() ? "string" : true)[0] ?? "error"; + (rand() ? "string" : true)[0] ?? "ok"; } function offsetAccessibleMaybeAndIllegal(): void From 6ed2ea64dee64f62d55ffae0e830464a87cc14ba Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 10:10:38 +0900 Subject: [PATCH 07/12] fix: move isOffsetAccessLegal out of trait --- src/Type/BooleanType.php | 5 +++++ src/Type/FloatType.php | 5 +++++ src/Type/IntegerType.php | 5 +++++ src/Type/NonexistentParentClassType.php | 5 +++++ src/Type/ResourceType.php | 5 +++++ src/Type/Traits/NonOffsetAccessibleTypeTrait.php | 5 ----- src/Type/VoidType.php | 5 +++++ 7 files changed, 30 insertions(+), 5 deletions(-) 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/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/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 5cf35ca527..a566e0c649 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::createYes(); + } + public function isScalar(): TrinaryLogic { return TrinaryLogic::createNo(); 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/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index 92e0a8c29b..d74dc8d0d1 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -14,11 +14,6 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createNo(); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createYes(); - } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createNo(); 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(); From dbfc3b85991831510b04e0b0b303becde2a2c7bd Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 10:19:12 +0900 Subject: [PATCH 08/12] fix: fix MaybeObjectType isOffsetAccessLegal --- src/Type/Accessory/HasMethodType.php | 5 +++++ src/Type/Accessory/HasPropertyType.php | 5 +++++ src/Type/CallableType.php | 5 +++++ src/Type/IterableType.php | 5 +++++ src/Type/ObjectShapeType.php | 5 +++++ src/Type/ObjectWithoutClassType.php | 5 +++++ src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php | 5 ----- 7 files changed, 30 insertions(+), 5 deletions(-) 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/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/CallableType.php b/src/Type/CallableType.php index c1cfe1a717..fe74ebed50 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::createYes(); + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index c675aff51d..ecb3c304c4 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::createYes(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); 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/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/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 485d62566f..6e83395e95 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -14,11 +14,6 @@ public function isOffsetAccessible(): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function isOffsetAccessLegal(): TrinaryLogic - { - return TrinaryLogic::createYes(); - } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic { return TrinaryLogic::createMaybe(); From 4b4af8e2648180122f15df45968db6e6411a126e Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 10:35:12 +0900 Subject: [PATCH 09/12] fix: callable and iterable are maybe not isOffsetAccessLegal --- src/Type/CallableType.php | 2 +- src/Type/IterableType.php | 2 +- .../Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index fe74ebed50..5986ed81fa 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -326,7 +326,7 @@ public function toArrayKey(): Type public function isOffsetAccessLegal(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createMaybe(); } public function getTemplateTypeMap(): TemplateTypeMap diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index ecb3c304c4..25c1ff3039 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -244,7 +244,7 @@ public function toArrayKey(): Type public function isOffsetAccessLegal(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createMaybe(); } public function isIterable(): TrinaryLogic diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 4cfba0a70d..d6f96d26db 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -678,6 +678,10 @@ public function testBug8068(): void "Cannot access offset 'path' on Closure.", 18, ], + [ + "Cannot access offset 'path' on iterable.", + 26, + ], ]); } From 631a3a1c2624d8f1d0fd2ccc4a8585dcbd5f313c Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 20:56:51 +0900 Subject: [PATCH 10/12] fix: NonexistentParentClassType is not offsetAccessLegal --- src/Type/NonexistentParentClassType.php | 2 +- .../Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 4 ++++ tests/PHPStan/Rules/Arrays/data/offset-access-legal.php | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index a566e0c649..b13f5d5105 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -154,7 +154,7 @@ public function toArrayKey(): Type public function isOffsetAccessLegal(): TrinaryLogic { - return TrinaryLogic::createYes(); + return TrinaryLogic::createNo(); } public function isScalar(): TrinaryLogic diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index d6f96d26db..e952abe0f5 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -800,6 +800,10 @@ public function testOffsetAccessLegal(): void 'Cannot access offset 0 on array{\'test\'}|(Closure(): void).', 98, ], + [ + 'Cannot access offset 0 on parent.', + 105, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php index 7d67dea8d2..1ad9077d98 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-legal.php @@ -97,3 +97,11 @@ function offsetAccessibleMaybeAndIllegal(): void (rand() ? function(){} : ['test'])[0] ?? "error"; } + +class Foo +{ + public function doBar(): void + { + (new parent())[0] ?? 'error'; + } +} From d7951a7e7aecc0aab4f66650edfe19d3d33acee5 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 21:08:13 +0900 Subject: [PATCH 11/12] test: update levels test behavior for array access to iterator --- tests/PHPStan/Levels/data/arrayDimFetches-7.json | 5 +++++ 1 file changed, 5 insertions(+) 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 From 9b18667b0b3aa786cf51ff1c49ae91657fa0c2e7 Mon Sep 17 00:00:00 2001 From: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Date: Tue, 7 May 2024 21:12:34 +0900 Subject: [PATCH 12/12] chore: fix lint --- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 9 ++++++++- .../data/offset-access-legal-non-existent-parent.php | 11 +++++++++++ .../PHPStan/Rules/Arrays/data/offset-access-legal.php | 8 -------- 3 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/offset-access-legal-non-existent-parent.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index e952abe0f5..a833f3489a 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -800,9 +800,16 @@ public function testOffsetAccessLegal(): void '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.', - 105, + 9, ], ]); } 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 @@ +