Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
18 changes: 13 additions & 5 deletions src/Rules/Arrays/AllowedArrayKeysTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,15 +22,22 @@
final class AllowedArrayKeysTypes
{

public static function getType(): Type
public static function getType(?PhpVersion $phpVersion = null): Type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why optional and nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used in ArrayType and ConstantArrayType

$allowedArrayKeys = AllowedArrayKeysTypes::getType();

and I don't have a PHPVersion here

{
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
Expand Down
9 changes: 9 additions & 0 deletions src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,7 @@ final class InvalidKeyInArrayDimFetchRule implements Rule

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private PhpVersion $phpVersion,
#[AutowiredParameter]
private bool $reportMaybes,
)
Expand Down Expand Up @@ -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 [];
}
Expand Down
7 changes: 5 additions & 2 deletions src/Rules/Arrays/InvalidKeyInArrayItemRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,7 @@ final class InvalidKeyInArrayItemRule implements Rule

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private PhpVersion $phpVersion,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More modern approach is to get PhpVersions from Scope (a separate class).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it means that all the method from PhpVersion should be deprecated/moved into PhpVersions ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet

)
{
}
Expand All @@ -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 [];
}
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Levels/data/stringOffsetAccess-7.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 28 additions & 3 deletions tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidKeyInArrayDimFetchRule>
Expand All @@ -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,
Expand Down Expand Up @@ -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')]
Expand Down
59 changes: 47 additions & 12 deletions tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidKeyInArrayItemRule>
Expand All @@ -21,50 +23,83 @@ 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
{
$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
Expand Down
18 changes: 13 additions & 5 deletions tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
namespace InvalidKeyArrayDimFetch;

$a = [];
$foo = $a[null];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved PHP-related errors to the end for an easier handling in tests.


$foo = $a[new \DateTimeImmutable()];
$a[[]] = $foo;
$a[1];
$a[1.0];

$a['1'];
$a[true];
$a[false];

/** @var string|null $stringOrNull */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed for a valid union here and move the invalid string|null to the end

$stringOrNull = doFoo();
$a[$stringOrNull];
/** @var string|int $stringOrInt */
$stringOrInt = doFoo();
$a[$stringOrInt];

$obj = new \SplObjectStorage();
$obj[new \stdClass()] = 1;
Expand Down Expand Up @@ -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];
7 changes: 6 additions & 1 deletion tests/PHPStan/Rules/Arrays/data/invalid-key-array-item.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
'foo',
1 => 'aaa',
'1' => 'aaa',
null => 'aaa',
new \DateTimeImmutable() => 'aaa',
[] => 'bbb',
$stringOrObject => 'aaa',
Expand All @@ -21,3 +20,9 @@
$b = [
$mixed => 'foo',
];

// PHP version dependent
$c = [
1.0 => 'aaa',
null => 'aaa',
];
Loading