From db744e74dc7a567ca1f52f76ab210a2872d32434 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 24 May 2025 13:44:05 +0200 Subject: [PATCH 1/3] Introduce reportCastedArrayKey parameter --- conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/Arrays/AllowedArrayKeysTypes.php | 17 +++++---- .../Arrays/InvalidKeyInArrayDimFetchRule.php | 4 ++- .../Arrays/InvalidKeyInArrayItemRule.php | 5 ++- .../InvalidKeyInArrayDimFetchRuleTest.php | 19 ++++++++++ .../Arrays/InvalidKeyInArrayItemRuleTest.php | 35 +++++++++++++++++++ .../Arrays/data/invalid-key-array-item.php | 4 +++ .../Rules/Arrays/data/unset-false-key.php | 16 +++++++++ 9 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 tests/PHPStan/Rules/Arrays/data/unset-false-key.php diff --git a/conf/config.neon b/conf/config.neon index ec0c6b03ac..22583962ec 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -79,6 +79,7 @@ parameters: reportStaticMethodSignatures: false reportWrongPhpDocTypeInVarTag: false reportAnyTypeWideningInVarTag: false + reportArrayKeyCast: false reportPossiblyNonexistentGeneralArrayOffset: false reportPossiblyNonexistentConstantArrayOffset: false checkMissingOverrideMethodAttribute: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 33fdd37b68..b256b1754e 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -88,6 +88,7 @@ parametersSchema: reportStaticMethodSignatures: bool() reportWrongPhpDocTypeInVarTag: bool() reportAnyTypeWideningInVarTag: bool() + reportArrayKeyCast: bool() reportPossiblyNonexistentGeneralArrayOffset: bool() reportPossiblyNonexistentConstantArrayOffset: bool() checkMissingOverrideMethodAttribute: bool() diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index b670dbb891..5ff38b7453 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -22,19 +22,22 @@ final class AllowedArrayKeysTypes { - public static function getType(?PhpVersion $phpVersion = null): Type + public static function getType(?PhpVersion $phpVersion = null, bool $strict = false): Type { $types = [ new IntegerType(), new StringType(), - new BooleanType(), ]; - if ($phpVersion === null || !$phpVersion->deprecatesImplicitlyFloatConversionToInt()) { - $types[] = new FloatType(); - } - if ($phpVersion === null || !$phpVersion->deprecatesNullArrayOffset()) { - $types[] = new NullType(); + if (!$strict) { + $types[] = new BooleanType(); + + if ($phpVersion === null || !$phpVersion->deprecatesImplicitlyFloatConversionToInt()) { + $types[] = new FloatType(); + } + if ($phpVersion === null || !$phpVersion->deprecatesNullArrayOffset()) { + $types[] = new NullType(); + } } return new UnionType($types); diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 0723234b36..ddd7963e92 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -27,6 +27,8 @@ public function __construct( private PhpVersion $phpVersion, #[AutowiredParameter] private bool $reportMaybes, + #[AutowiredParameter] + private bool $reportArrayKeyCast, ) { } @@ -58,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion); + $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->reportArrayKeyCast); $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->dim, diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index 298c65b565..3b875a6703 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; @@ -24,6 +25,8 @@ final class InvalidKeyInArrayItemRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $reportArrayKeyCast, ) { } @@ -39,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion); + $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->reportArrayKeyCast); $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->key, diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 38baa0026d..d63cc0eb32 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -15,6 +15,8 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase { + private bool $reportCastedArrayKey = false; + protected function getRule(): Rule { $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, true, true, false, true); @@ -22,6 +24,7 @@ protected function getRule(): Rule $ruleLevelHelper, self::getContainer()->getByType(PhpVersion::class), true, + $this->reportCastedArrayKey, ); } @@ -163,4 +166,20 @@ public function testBug12981(): void ]); } + public function testUnsetFalseKey(): void + { + $this->reportCastedArrayKey = true; + + $this->analyse([__DIR__ . '/data/unset-false-key.php'], [ + [ + 'Invalid array key type false.', + 6, + ], + [ + 'Invalid array key type false.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 1c01cf62ca..efa5df3e7b 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -15,6 +15,8 @@ class InvalidKeyInArrayItemRuleTest extends RuleTestCase { + private bool $reportCastedArrayKey = false; + private bool $checkExplicitMixed = false; private bool $checkImplicitMixed = false; @@ -26,6 +28,7 @@ protected function getRule(): Rule return new InvalidKeyInArrayItemRule( $ruleLevelHelper, self::getContainer()->getByType(PhpVersion::class), + $this->reportCastedArrayKey, ); } @@ -102,6 +105,38 @@ public function testInvalidMixedKey(): void $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], $errors); } + public function testInvalidKeyReportingCastedArrayKey(): void + { + $this->reportCastedArrayKey = true; + + $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], [ + [ + 'Invalid array key type DateTimeImmutable.', + 12, + ], + [ + 'Invalid array key type array.', + 13, + ], + [ + 'Possibly invalid array key type stdClass|string.', + 14, + ], + [ + 'Invalid array key type float.', + 26, + ], + [ + 'Invalid array key type null.', + 27, + ], + [ + 'Invalid array key type false.', + 31, + ], + ]); + } + public function testInvalidKeyInList(): void { $this->analyse([__DIR__ . '/data/invalid-key-list.php'], [ diff --git a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php index 1c734968b2..e7ebc67ad6 100644 --- a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php @@ -26,3 +26,7 @@ 1.0 => 'aaa', null => 'aaa', ]; + +$d = [ + false => 'aaa', +]; diff --git a/tests/PHPStan/Rules/Arrays/data/unset-false-key.php b/tests/PHPStan/Rules/Arrays/data/unset-false-key.php new file mode 100644 index 0000000000..d47f059600 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/unset-false-key.php @@ -0,0 +1,16 @@ + $data */ +unset($data[false]); + +function test_remove_element(): void { + $modified = [1, 4, 6, 8]; + + // this would happen in the SUT + unset($modified[array_search(4, $modified, true)]); + unset($modified[array_search(5, $modified, true)]); // bug is here - will unset key `0` by accident + + assert([1, 6, 8] === $modified); // actually is [6, 8] +} From 25003b78552ad44ee057f0d8a5cc9b0c07c46ef8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 9 Oct 2025 18:34:53 +0200 Subject: [PATCH 2/3] Rename --- conf/config.neon | 2 +- conf/parametersSchema.neon | 2 +- src/Rules/Arrays/AllowedArrayKeysTypes.php | 4 ++-- src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php | 4 ++-- src/Rules/Arrays/InvalidKeyInArrayItemRule.php | 4 ++-- .../Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php | 6 +++--- .../PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index 22583962ec..d2cb8433b1 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -79,7 +79,7 @@ parameters: reportStaticMethodSignatures: false reportWrongPhpDocTypeInVarTag: false reportAnyTypeWideningInVarTag: false - reportArrayKeyCast: false + allowFloatBoolNullAsArrayKey: true reportPossiblyNonexistentGeneralArrayOffset: false reportPossiblyNonexistentConstantArrayOffset: false checkMissingOverrideMethodAttribute: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index b256b1754e..389815ec3d 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -88,7 +88,7 @@ parametersSchema: reportStaticMethodSignatures: bool() reportWrongPhpDocTypeInVarTag: bool() reportAnyTypeWideningInVarTag: bool() - reportArrayKeyCast: bool() + allowFloatBoolNullAsArrayKey: bool() reportPossiblyNonexistentGeneralArrayOffset: bool() reportPossiblyNonexistentConstantArrayOffset: bool() checkMissingOverrideMethodAttribute: bool() diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index 5ff38b7453..bd494b99a1 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -22,14 +22,14 @@ final class AllowedArrayKeysTypes { - public static function getType(?PhpVersion $phpVersion = null, bool $strict = false): Type + public static function getType(?PhpVersion $phpVersion = null, bool $allowFloatBoolNull = true): Type { $types = [ new IntegerType(), new StringType(), ]; - if (!$strict) { + if (!$allowFloatBoolNull) { $types[] = new BooleanType(); if ($phpVersion === null || !$phpVersion->deprecatesImplicitlyFloatConversionToInt()) { diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index ddd7963e92..120a98ce58 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -28,7 +28,7 @@ public function __construct( #[AutowiredParameter] private bool $reportMaybes, #[AutowiredParameter] - private bool $reportArrayKeyCast, + private bool $allowFloatBoolNullAsArrayKey, ) { } @@ -60,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->reportArrayKeyCast); + $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->allowFloatBoolNullAsArrayKey); $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->dim, diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index 3b875a6703..79216ed3a3 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -26,7 +26,7 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, #[AutowiredParameter] - private bool $reportArrayKeyCast, + private bool $allowFloatBoolNullAsArrayKey, ) { } @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->reportArrayKeyCast); + $allowedArrayKeys = AllowedArrayKeysTypes::getType($this->phpVersion, $this->allowFloatBoolNullAsArrayKey); $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->key, diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index d63cc0eb32..fb9ba3dceb 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -15,7 +15,7 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase { - private bool $reportCastedArrayKey = false; + private bool $allowFloatBoolNullAsArrayKey = true; protected function getRule(): Rule { @@ -24,7 +24,7 @@ protected function getRule(): Rule $ruleLevelHelper, self::getContainer()->getByType(PhpVersion::class), true, - $this->reportCastedArrayKey, + $this->allowFloatBoolNullAsArrayKey, ); } @@ -168,7 +168,7 @@ public function testBug12981(): void public function testUnsetFalseKey(): void { - $this->reportCastedArrayKey = true; + $this->allowFloatBoolNullAsArrayKey = false; $this->analyse([__DIR__ . '/data/unset-false-key.php'], [ [ diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index efa5df3e7b..cd2c5d45b5 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -15,7 +15,7 @@ class InvalidKeyInArrayItemRuleTest extends RuleTestCase { - private bool $reportCastedArrayKey = false; + private bool $allowFloatBoolNullAsArrayKey = true; private bool $checkExplicitMixed = false; @@ -28,7 +28,7 @@ protected function getRule(): Rule return new InvalidKeyInArrayItemRule( $ruleLevelHelper, self::getContainer()->getByType(PhpVersion::class), - $this->reportCastedArrayKey, + $this->allowFloatBoolNullAsArrayKey, ); } @@ -107,7 +107,7 @@ public function testInvalidMixedKey(): void public function testInvalidKeyReportingCastedArrayKey(): void { - $this->reportCastedArrayKey = true; + $this->allowFloatBoolNullAsArrayKey = false; $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], [ [ From 5aeddaa3ca5aad8ea5008bf2b7d8afbd30b91ebf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 9 Oct 2025 18:38:44 +0200 Subject: [PATCH 3/3] Fix --- src/Rules/Arrays/AllowedArrayKeysTypes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index bd494b99a1..3cae964952 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -29,7 +29,7 @@ public static function getType(?PhpVersion $phpVersion = null, bool $allowFloatB new StringType(), ]; - if (!$allowFloatBoolNull) { + if ($allowFloatBoolNull) { $types[] = new BooleanType(); if ($phpVersion === null || !$phpVersion->deprecatesImplicitlyFloatConversionToInt()) {