From 456c943c5ee980edbb8d1e6f827baf304c94b2d5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 4 Oct 2025 12:58:50 +0200 Subject: [PATCH] Report float and null offset based on PHP version --- src/Php/PhpVersion.php | 10 ++++ src/Rules/Arrays/AllowedArrayKeysTypes.php | 18 ++++-- .../DuplicateKeysInLiteralArraysRule.php | 9 +++ .../Arrays/InvalidKeyInArrayDimFetchRule.php | 7 ++- .../Arrays/InvalidKeyInArrayItemRule.php | 7 ++- .../Levels/data/stringOffsetAccess-7.json | 5 ++ .../InvalidKeyInArrayDimFetchRuleTest.php | 31 +++++++++- .../Arrays/InvalidKeyInArrayItemRuleTest.php | 59 +++++++++++++++---- .../data/invalid-key-array-dim-fetch.php | 18 ++++-- .../Arrays/data/invalid-key-array-item.php | 7 ++- 10 files changed, 141 insertions(+), 30 deletions(-) diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 2a151b58ae..fc741647a8 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -414,4 +414,14 @@ public function hasPDOSubclasses(): bool return $this->versionId >= 80400; } + public function deprecatesImplicitlyFloatConversionToInt(): bool + { + return $this->versionId >= 80100; + } + + public function deprecatesNullArrayOffset(): bool + { + return $this->versionId >= 80500; + } + } diff --git a/src/Rules/Arrays/AllowedArrayKeysTypes.php b/src/Rules/Arrays/AllowedArrayKeysTypes.php index 7d920e167c..b670dbb891 100644 --- a/src/Rules/Arrays/AllowedArrayKeysTypes.php +++ b/src/Rules/Arrays/AllowedArrayKeysTypes.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Php\PhpVersion; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -21,15 +22,22 @@ final class AllowedArrayKeysTypes { - public static function getType(): Type + public static function getType(?PhpVersion $phpVersion = null): Type { - return new UnionType([ + $types = [ new IntegerType(), new StringType(), - new FloatType(), new BooleanType(), - new NullType(), - ]); + ]; + + if ($phpVersion === null || !$phpVersion->deprecatesImplicitlyFloatConversionToInt()) { + $types[] = new FloatType(); + } + if ($phpVersion === null || !$phpVersion->deprecatesNullArrayOffset()) { + $types[] = new NullType(); + } + + return new UnionType($types); } public static function narrowOffsetKeyType(Type $varType, Type $keyType): ?Type diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index c05440f892..875980c02d 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -15,6 +15,8 @@ use function array_search; use function count; use function implode; +use function is_bool; +use function is_float; use function is_int; use function max; use function sprintf; @@ -128,6 +130,13 @@ public function processNode(Node $node, Scope $scope): array } foreach ($keyValues as $value) { + // Prevent php warning by manually casting array keys + if (is_bool($value) || is_float($value)) { + $value = (int) $value; + } elseif ($value === null) { + $value = (string) $value; + } + $printedValue = $key !== null ? $this->exprPrinter->printExpr($key) : $value; diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 5ba1a88009..50e74ca81a 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -23,6 +24,7 @@ final class InvalidKeyInArrayDimFetchRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, + private PhpVersion $phpVersion, #[AutowiredParameter] private bool $reportMaybes, ) @@ -56,17 +58,18 @@ public function processNode(Node $node, Scope $scope): array return []; } + $phpVersion = $this->phpVersion; $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->dim, '', - static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimType)->yes(), + static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType($phpVersion)->isSuperTypeOf($dimType)->yes(), )->getType(); if ($dimensionType instanceof ErrorType) { return []; } - $isSuperType = AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType); + $isSuperType = AllowedArrayKeysTypes::getType($phpVersion)->isSuperTypeOf($dimensionType); if ($isSuperType->yes() || ($isSuperType->maybe() && !$this->reportMaybes)) { return []; } diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index a4c76cfc8a..ed47303512 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -22,6 +23,7 @@ final class InvalidKeyInArrayItemRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, + private PhpVersion $phpVersion, ) { } @@ -37,17 +39,18 @@ public function processNode(Node $node, Scope $scope): array return []; } + $phpVersion = $this->phpVersion; $dimensionType = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->key, '', - static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimType)->yes(), + static fn (Type $dimType): bool => AllowedArrayKeysTypes::getType($phpVersion)->isSuperTypeOf($dimType)->yes(), )->getType(); if ($dimensionType instanceof ErrorType) { return []; } - $isSuperType = AllowedArrayKeysTypes::getType()->isSuperTypeOf($dimensionType); + $isSuperType = AllowedArrayKeysTypes::getType($phpVersion)->isSuperTypeOf($dimensionType); if ($isSuperType->yes()) { return []; } diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json index cfe8c12f38..5dd7f36212 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-7.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-7.json @@ -14,6 +14,11 @@ "line": 31, "ignorable": true }, + { + "message": "Possibly invalid array key type float.", + "line": 31, + "ignorable": true + }, { "message": "Offset int|object might not exist on array|string.", "line": 35, diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 22716afcd4..74d85a22f7 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -16,12 +18,16 @@ class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase protected function getRule(): Rule { $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, true, true, false, true); - return new InvalidKeyInArrayDimFetchRule($ruleLevelHelper, true); + return new InvalidKeyInArrayDimFetchRule( + $ruleLevelHelper, + self::getContainer()->getByType(PhpVersion::class), + true, + ); } public function testInvalidKey(): void { - $this->analyse([__DIR__ . '/data/invalid-key-array-dim-fetch.php'], [ + $errors = [ [ 'Invalid array key type DateTimeImmutable.', 7, @@ -62,7 +68,26 @@ public function testInvalidKey(): void 'Invalid array key type DateTimeImmutable.', 48, ], - ]); + ]; + + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Invalid array key type float.', + 51, + ]; + } + if (PHP_VERSION_ID >= 80500) { + $errors[] = [ + 'Invalid array key type null.', + 52, + ]; + $errors[] = [ + 'Possibly invalid array key type string|null.', + 56, + ]; + } + + $this->analyse([__DIR__ . '/data/invalid-key-array-dim-fetch.php'], $errors); } #[RequiresPhp('>= 8.1')] diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 17664a61f8..1c01cf62ca 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,25 +23,43 @@ protected function getRule(): Rule { $ruleLevelHelper = new RuleLevelHelper(self::createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); - return new InvalidKeyInArrayItemRule($ruleLevelHelper); + return new InvalidKeyInArrayItemRule( + $ruleLevelHelper, + self::getContainer()->getByType(PhpVersion::class), + ); } public function testInvalidKey(): void { - $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], [ + $errors = [ [ 'Invalid array key type DateTimeImmutable.', - 13, + 12, ], [ 'Invalid array key type array.', - 14, + 13, ], [ 'Possibly invalid array key type stdClass|string.', - 15, + 14, ], - ]); + ]; + + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Invalid array key type float.', + 26, + ]; + } + if (PHP_VERSION_ID >= 80500) { + $errors[] = [ + 'Invalid array key type null.', + 27, + ]; + } + + $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], $errors); } public function testInvalidMixedKey(): void @@ -47,24 +67,39 @@ public function testInvalidMixedKey(): void $this->checkExplicitMixed = true; $this->checkImplicitMixed = true; - $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], [ + $errors = [ [ 'Invalid array key type DateTimeImmutable.', - 13, + 12, ], [ 'Invalid array key type array.', - 14, + 13, ], [ 'Possibly invalid array key type stdClass|string.', - 15, + 14, ], [ 'Possibly invalid array key type mixed.', - 22, + 21, ], - ]); + ]; + + if (PHP_VERSION_ID >= 80100) { + $errors[] = [ + 'Invalid array key type float.', + 26, + ]; + } + if (PHP_VERSION_ID >= 80500) { + $errors[] = [ + 'Invalid array key type null.', + 27, + ]; + } + + $this->analyse([__DIR__ . '/data/invalid-key-array-item.php'], $errors); } public function testInvalidKeyInList(): void diff --git a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php index fb7514c6cb..70d92f4d27 100644 --- a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php @@ -3,18 +3,18 @@ namespace InvalidKeyArrayDimFetch; $a = []; -$foo = $a[null]; + $foo = $a[new \DateTimeImmutable()]; $a[[]] = $foo; $a[1]; -$a[1.0]; + $a['1']; $a[true]; $a[false]; -/** @var string|null $stringOrNull */ -$stringOrNull = doFoo(); -$a[$stringOrNull]; +/** @var string|int $stringOrInt */ +$stringOrInt = doFoo(); +$a[$stringOrInt]; $obj = new \SplObjectStorage(); $obj[new \stdClass()] = 1; @@ -46,3 +46,11 @@ $array[5][new \DateTimeImmutable()]; $array[new \stdClass()][new \DateTimeImmutable()]; $array[new \DateTimeImmutable()][] = 5; + +// Php version dependant +$a[1.0]; +$foo = $a[null]; + +/** @var string|null $stringOrNull */ +$stringOrNull = doFoo(); +$a[$stringOrNull]; 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 0343a0b1ad..1c734968b2 100644 --- a/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php +++ b/tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php @@ -9,7 +9,6 @@ 'foo', 1 => 'aaa', '1' => 'aaa', - null => 'aaa', new \DateTimeImmutable() => 'aaa', [] => 'bbb', $stringOrObject => 'aaa', @@ -21,3 +20,9 @@ $b = [ $mixed => 'foo', ]; + +// PHP version dependent +$c = [ + 1.0 => 'aaa', + null => 'aaa', +];