Skip to content

Commit c057aa9

Browse files
committed
Make ArrayKeyExistsFunctionTypeSpecifyingExtension understand an empty array
1 parent 9545f03 commit c057aa9

File tree

5 files changed

+50
-12
lines changed

5 files changed

+50
-12
lines changed

src/Type/ObjectType.php

+6
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ public function toArray(): Type
540540
$arrayKeys = [];
541541
$arrayValues = [];
542542

543+
$isFinal = $classReflection->isFinal();
544+
543545
do {
544546
foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) {
545547
if ($nativeProperty->isStatic()) {
@@ -570,6 +572,10 @@ public function toArray(): Type
570572
$classReflection = $classReflection->getParentClass();
571573
} while ($classReflection !== null);
572574

575+
if (!$isFinal && count($arrayKeys) === 0) {
576+
return new ArrayType(new MixedType(), new MixedType());
577+
}
578+
573579
return new ConstantArrayType($arrayKeys, $arrayValues);
574580
}
575581

src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ public function specifyTypes(
5555
$key = $node->getArgs()[0]->value;
5656
$array = $node->getArgs()[1]->value;
5757
$keyType = $scope->getType($key);
58-
if (!$keyType instanceof ConstantIntegerType && !$keyType instanceof ConstantStringType) {
58+
$arrayType = $scope->getType($array);
59+
if (!$keyType instanceof ConstantIntegerType && !$keyType instanceof ConstantStringType && !$arrayType->isIterableAtLeastOnce()->no()) {
5960
if ($context->truthy()) {
6061
$arrayDimFetch = new ArrayDimFetch(
6162
$array,
6263
$key,
6364
);
64-
return $this->typeSpecifier->create($arrayDimFetch, $scope->getType($array)->getIterableValueType(), $context, false, $scope, new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))));
65+
return $this->typeSpecifier->create($arrayDimFetch, $arrayType->getIterableValueType(), $context, false, $scope, new Identical($arrayDimFetch, new ConstFetch(new Name('__PHPSTAN_FAUX_CONSTANT'))));
6566
}
6667

6768
return new SpecifiedTypes();

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,7 @@ public function dataFileAsserts(): iterable
10061006
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-unset-bug.php');
10071007
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6170.php');
10081008
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php');
1009+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key-exists.php');
10091010
}
10101011

10111012
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace ArrayKeyExistsExtension;
4+
5+
use function array_key_exists;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param array<string, string> $a
13+
* @return void
14+
*/
15+
public function doFoo(array $a, string $key): void
16+
{
17+
assertType('false', array_key_exists(2, $a));
18+
assertType('bool', array_key_exists('foo', $a));
19+
assertType('false', array_key_exists('2', $a));
20+
21+
$a = ['foo' => 2, 3 => 'bar'];
22+
assertType('true', array_key_exists('foo', $a));
23+
assertType('true', array_key_exists('3', $a));
24+
assertType('false', array_key_exists(4, $a));
25+
26+
$empty = [];
27+
assertType('false', array_key_exists('foo', $empty));
28+
assertType('false', array_key_exists($key, $empty));
29+
}
30+
31+
}

tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php

+9-10
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,26 @@ public function doFoo(array $percentageIntervals, array $changes): void
1515
if ($percentageInterval->isInInterval((float) $changeInPercents)) {
1616
$key = $percentageInterval->getFormatted();
1717
if (array_key_exists($key, $intervalResults)) {
18-
assertType('aaa', $intervalResults);
19-
assertType('aaa', $intervalResults[$key]);
18+
assertType('array<int|string, array{itemsCount: mixed, interval: mixed}>', $intervalResults);
19+
assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]);
2020
$intervalResults[$key]['itemsCount'] += $itemsCount;
21-
assertType('aaa', $intervalResults);
22-
assertType('aaa', $intervalResults[$key]);
21+
assertType('non-empty-array<int|string, array{itemsCount: (array|float|int), interval: mixed}>', $intervalResults);
22+
assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]);
2323
} else {
24-
assertType('aaa', $intervalResults);
25-
assertType('aaa', $intervalResults[$key]);
24+
assertType('array<int|string, array{itemsCount: mixed, interval: mixed}>', $intervalResults);
25+
assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]);
2626
$intervalResults[$key] = [
2727
'itemsCount' => $itemsCount,
2828
'interval' => $percentageInterval,
2929
];
30-
assertType('aaa', $intervalResults);
31-
assertType('aaa', $intervalResults[$key]);
30+
assertType('non-empty-array<int|string, array{itemsCount: mixed, interval: mixed}>', $intervalResults);
31+
assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]);
3232
}
3333
}
3434
}
3535
}
3636

37-
assertType('aaa', $intervalResults);
38-
assertType('aaa', $intervalResults);
37+
assertType('array<int|string, array{itemsCount: mixed, interval: mixed}>', $intervalResults);
3938
foreach ($intervalResults as $data) {
4039
echo $data['interval'];
4140
}

0 commit comments

Comments
 (0)