diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 30f7febe49..5ba1a88009 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -41,19 +40,29 @@ public function processNode(Node $node, Scope $scope): array return []; } - $dimensionType = $scope->getType($node->dim); - if ($dimensionType instanceof MixedType) { - return []; - } - $varType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->var, '', - static fn (Type $varType): bool => $varType->isArray()->no() || AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType)->yes(), + static fn (Type $varType): bool => $varType->isArray()->no(), )->getType(); - if ($varType instanceof ErrorType || $varType->isArray()->no()) { + if ($varType instanceof ErrorType) { + return []; + } + + $isArray = $varType->isArray(); + if ($isArray->no() || ($isArray->maybe() && !$this->reportMaybes)) { + return []; + } + + $dimensionType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->dim, + '', + static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimType)->yes(), + )->getType(); + if ($dimensionType instanceof ErrorType) { return []; } @@ -64,7 +73,11 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( - sprintf('%s array key type %s.', $isSuperType->no() ? 'Invalid' : 'Possibly invalid', $dimensionType->describe(VerbosityLevel::typeOnly())), + sprintf( + '%s array key type %s.', + $isArray->yes() && $isSuperType->no() ? 'Invalid' : 'Possibly invalid', + $dimensionType->describe(VerbosityLevel::typeOnly()), + ), )->identifier('offsetAccess.invalidOffset')->build(), ]; } diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 109b30a2b3..d82ff0745b 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -190,7 +190,11 @@ private function findTypeToCheckImplementation( bool $isTopLevel = false, ): FoundTypeResult { - if (!$this->checkNullables && !$type->isNull()->yes()) { + if ( + !$this->checkNullables + && !$type->isNull()->yes() + && !$unionTypeCriteriaCallback(new NullType()) + ) { $type = TypeCombinator::removeNull($type); } diff --git a/tests/PHPStan/Levels/LevelsIntegrationTest.php b/tests/PHPStan/Levels/LevelsIntegrationTest.php index 12499dce44..044a088f60 100644 --- a/tests/PHPStan/Levels/LevelsIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsIntegrationTest.php @@ -41,6 +41,7 @@ public static function dataTopics(): array ['arrayDestructuring'], ['listType'], ['missingTypes'], + ['arrayOffsetAccess'], ]; if (PHP_VERSION_ID >= 80300) { $topics[] = ['constantAccesses83']; diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json new file mode 100644 index 0000000000..e0b5d1d2d8 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-10.json @@ -0,0 +1,52 @@ +[ + { + "message": "Possibly invalid array key type mixed.", + "line": 22, + "ignorable": true + }, + { + "message": "Possibly invalid array key type mixed.", + "line": 31, + "ignorable": true + }, + { + "message": "Cannot access offset 42 on mixed.", + "line": 42, + "ignorable": true + }, + { + "message": "Cannot access offset null on mixed.", + "line": 43, + "ignorable": true + }, + { + "message": "Cannot access offset DateTimeImmutable on mixed.", + "line": 44, + "ignorable": true + }, + { + "message": "Cannot access offset int|null on mixed.", + "line": 45, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 46, + "ignorable": true + }, + { + "message": "Cannot access offset object|null on mixed.", + "line": 47, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 48, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 49, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json new file mode 100644 index 0000000000..3e23caff09 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-3.json @@ -0,0 +1,7 @@ +[ + { + "message": "Invalid array key type DateTimeImmutable.", + "line": 17, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json new file mode 100644 index 0000000000..3f91016ab3 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-4.json @@ -0,0 +1,142 @@ +[ + { + "message": "Expression \"$a[42]\" on a separate line does not do anything.", + "line": 15, + "ignorable": true + }, + { + "message": "Expression \"$a[null]\" on a separate line does not do anything.", + "line": 16, + "ignorable": true + }, + { + "message": "Expression \"$a[$intOrNull]\" on a separate line does not do anything.", + "line": 18, + "ignorable": true + }, + { + "message": "Expression \"$a[$objectOrInt]\" on a separate line does not do anything.", + "line": 19, + "ignorable": true + }, + { + "message": "Expression \"$a[$objectOrNull]\" on a separate line does not do anything.", + "line": 20, + "ignorable": true + }, + { + "message": "Expression \"$a[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 21, + "ignorable": true + }, + { + "message": "Expression \"$a[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 22, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[42]\" on a separate line does not do anything.", + "line": 24, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[null]\" on a separate line does not do anything.", + "line": 25, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$intOrNull]\" on a separate line does not do anything.", + "line": 27, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$objectOrInt]\" on a separate line does not do anything.", + "line": 28, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$objectOrNull]\" on a separate line does not do anything.", + "line": 29, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 30, + "ignorable": true + }, + { + "message": "Expression \"$arrayOrObject[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 31, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[42]\" on a separate line does not do anything.", + "line": 33, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[null]\" on a separate line does not do anything.", + "line": 34, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$intOrNull]\" on a separate line does not do anything.", + "line": 36, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$objectOrInt]\" on a separate line does not do anything.", + "line": 37, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$objectOrNull]\" on a separate line does not do anything.", + "line": 38, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 39, + "ignorable": true + }, + { + "message": "Expression \"$explicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 40, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[42]\" on a separate line does not do anything.", + "line": 42, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[null]\" on a separate line does not do anything.", + "line": 43, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$intOrNull]\" on a separate line does not do anything.", + "line": 45, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$objectOrInt]\" on a separate line does not do anything.", + "line": 46, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$objectOrNull]\" on a separate line does not do anything.", + "line": 47, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$explicitlyMixed]\" on a separate line does not do anything.", + "line": 48, + "ignorable": true + }, + { + "message": "Expression \"$implicitlyMixed[$implicitlyMixed]\" on a separate line does not do anything.", + "line": 49, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json new file mode 100644 index 0000000000..5c3bc3d299 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-6.json @@ -0,0 +1,22 @@ +[ + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $a with no value type specified in iterable type array.", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $arrayOrObject with generic interface ArrayAccess but does not specify its types: TKey, TValue", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $arrayOrObject with no value type specified in iterable type array.", + "line": 13, + "ignorable": true + }, + { + "message": "Method Levels\\ArrayOffsetAccess\\Foo::test() has parameter $implicitlyMixed with no type specified.", + "line": 13, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json new file mode 100644 index 0000000000..674057be06 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-7.json @@ -0,0 +1,27 @@ +[ + { + "message": "Possibly invalid array key type int|object.", + "line": 19, + "ignorable": true + }, + { + "message": "Possibly invalid array key type object|null.", + "line": 20, + "ignorable": true + }, + { + "message": "Possibly invalid array key type DateTimeImmutable.", + "line": 26, + "ignorable": true + }, + { + "message": "Possibly invalid array key type int|object.", + "line": 28, + "ignorable": true + }, + { + "message": "Possibly invalid array key type object|null.", + "line": 29, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json b/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json new file mode 100644 index 0000000000..edca311940 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess-9.json @@ -0,0 +1,52 @@ +[ + { + "message": "Possibly invalid array key type mixed.", + "line": 21, + "ignorable": true + }, + { + "message": "Possibly invalid array key type mixed.", + "line": 30, + "ignorable": true + }, + { + "message": "Cannot access offset 42 on mixed.", + "line": 33, + "ignorable": true + }, + { + "message": "Cannot access offset null on mixed.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot access offset DateTimeImmutable on mixed.", + "line": 35, + "ignorable": true + }, + { + "message": "Cannot access offset int|null on mixed.", + "line": 36, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 37, + "ignorable": true + }, + { + "message": "Cannot access offset object|null on mixed.", + "line": 38, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 39, + "ignorable": true + }, + { + "message": "Cannot access offset mixed on mixed.", + "line": 40, + "ignorable": true + } +] diff --git a/tests/PHPStan/Levels/data/arrayOffsetAccess.php b/tests/PHPStan/Levels/data/arrayOffsetAccess.php new file mode 100644 index 0000000000..362bb5c03f --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayOffsetAccess.php @@ -0,0 +1,51 @@ +