Skip to content

Commit bab0af7

Browse files
authored
!array_key_exists() should imply array for PHP8+
1 parent 300b7b2 commit bab0af7

14 files changed

+291
-10
lines changed

src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1414
use PHPStan\Analyser\TypeSpecifierContext;
1515
use PHPStan\DependencyInjection\AutowiredService;
16+
use PHPStan\Php\PhpVersion;
1617
use PHPStan\Reflection\FunctionReflection;
1718
use PHPStan\Type\Accessory\HasOffsetType;
1819
use PHPStan\Type\Accessory\NonEmptyArrayType;
@@ -31,6 +32,12 @@ final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTyp
3132

3233
private TypeSpecifier $typeSpecifier;
3334

35+
public function __construct(
36+
private PhpVersion $phpVersion,
37+
)
38+
{
39+
}
40+
3441
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
3542
{
3643
$this->typeSpecifier = $typeSpecifier;
@@ -116,6 +123,22 @@ public function specifyTypes(
116123
new ArrayType(new MixedType(), new MixedType()),
117124
new HasOffsetType($keyType),
118125
);
126+
} elseif ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
127+
$specifiedTypes = $this->typeSpecifier->create(
128+
$array,
129+
new HasOffsetType($keyType),
130+
$context,
131+
$scope,
132+
);
133+
134+
$type = new ArrayType(new MixedType(), new MixedType());
135+
$type = $type->unsetOffset($keyType);
136+
return $specifiedTypes->unionWith($this->typeSpecifier->create(
137+
$array,
138+
$type,
139+
$context->negate(),
140+
$scope,
141+
));
119142
} else {
120143
$type = new HasOffsetType($keyType);
121144
}

tests/PHPStan/Analyser/TypeSpecifierTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ public static function dataCondition(): iterable
10401040
'$array' => 'non-empty-array',
10411041
],
10421042
[
1043-
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
1043+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
10441044
],
10451045
],
10461046
[
@@ -1055,7 +1055,7 @@ public static function dataCondition(): iterable
10551055
]),
10561056
)),
10571057
[
1058-
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
1058+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
10591059
],
10601060
[
10611061
'$array' => 'non-empty-array',
@@ -1070,7 +1070,7 @@ public static function dataCondition(): iterable
10701070
'$array' => 'non-empty-array&hasOffset(\'foo\')',
10711071
],
10721072
[
1073-
'$array' => '~hasOffset(\'foo\')',
1073+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('foo')",
10741074
],
10751075
],
10761076
[
@@ -1088,7 +1088,7 @@ public static function dataCondition(): iterable
10881088
'$array' => 'non-empty-array',
10891089
],
10901090
[
1091-
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
1091+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
10921092
],
10931093
],
10941094
[
@@ -1103,7 +1103,7 @@ public static function dataCondition(): iterable
11031103
]),
11041104
)),
11051105
[
1106-
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
1106+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
11071107
],
11081108
[
11091109
'$array' => 'non-empty-array',
@@ -1118,7 +1118,7 @@ public static function dataCondition(): iterable
11181118
'$array' => 'non-empty-array&hasOffset(\'foo\')',
11191119
],
11201120
[
1121-
'$array' => '~hasOffset(\'foo\')',
1121+
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('foo')",
11221122
],
11231123
],
11241124
[
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug13270bPhp8;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Test
10+
{
11+
/**
12+
* @param mixed[] $data
13+
* @return mixed[]
14+
*/
15+
public function parseData(array $data): array
16+
{
17+
if (isset($data['price'])) {
18+
assertType('mixed~null', $data['price']);
19+
if (!array_key_exists('priceWithVat', $data['price'])) {
20+
$data['price']['priceWithVat'] = null;
21+
}
22+
assertType("non-empty-array&hasOffsetValue('priceWithVat', mixed)", $data['price']);
23+
if (!array_key_exists('priceWithoutVat', $data['price'])) {
24+
$data['price']['priceWithoutVat'] = null;
25+
}
26+
assertType("non-empty-array&hasOffsetValue('priceWithoutVat', mixed)&hasOffsetValue('priceWithVat', mixed)", $data['price']);
27+
}
28+
return $data;
29+
}
30+
}

tests/PHPStan/Analyser/nsrt/bug-13270b.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<?php declare(strict_types=1);
1+
<?php // lint < 8.0
2+
3+
declare(strict_types=1);
24

35
namespace Bug13270b;
46

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug13301Php8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo($mixed) {
8+
if (array_key_exists('a', $mixed)) {
9+
assertType("non-empty-array&hasOffset('a')", $mixed);
10+
echo "has-a";
11+
} else {
12+
assertType("array<mixed~'a', mixed>", $mixed);
13+
echo "NO-a";
14+
}
15+
assertType('array', $mixed);
16+
}
17+
18+
function doFooTrue($mixed) {
19+
if (array_key_exists('a', $mixed) === true) {
20+
assertType("non-empty-array&hasOffset('a')", $mixed);
21+
} else {
22+
assertType("array<mixed~'a', mixed>", $mixed);
23+
}
24+
assertType('array', $mixed);
25+
}
26+
27+
function doFooTruethy($mixed) {
28+
if (array_key_exists('a', $mixed) == true) {
29+
assertType("non-empty-array&hasOffset('a')", $mixed);
30+
} else {
31+
assertType("array<mixed~'a', mixed>", $mixed);
32+
}
33+
assertType('array', $mixed);
34+
}
35+
36+
function doFooFalsey($mixed) {
37+
if (array_key_exists('a', $mixed) == 0) {
38+
assertType("array<mixed~'a', mixed>", $mixed);
39+
} else {
40+
assertType("non-empty-array&hasOffset('a')", $mixed);
41+
}
42+
assertType('array', $mixed);
43+
}
44+
45+
function doArray(array $arr) {
46+
if (array_key_exists('a', $arr)) {
47+
assertType("non-empty-array&hasOffset('a')", $arr);
48+
} else {
49+
assertType("array<mixed~'a', mixed>", $arr);
50+
}
51+
assertType('array', $arr);
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php // lint < 8.0
2+
3+
namespace Bug13301Php7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo($mixed) {
8+
if (array_key_exists('a', $mixed)) {
9+
assertType("non-empty-array&hasOffset('a')", $mixed);
10+
echo "has-a";
11+
} else {
12+
assertType("mixed~hasOffset('a')", $mixed);
13+
echo "NO-a";
14+
}
15+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug2001Php8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
public function parseUrl(string $url): string
10+
{
11+
$parsedUrl = parse_url(urldecode($url));
12+
assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl);
13+
14+
if (array_key_exists('host', $parsedUrl)) {
15+
assertType('array{scheme?: string, host: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);
16+
throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.');
17+
}
18+
19+
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);
20+
21+
$redirectUrl = $parsedUrl['path'];
22+
23+
if (array_key_exists('query', $parsedUrl)) {
24+
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $parsedUrl);
25+
$redirectUrl .= '?' . $parsedUrl['query'];
26+
}
27+
28+
if (array_key_exists('fragment', $parsedUrl)) {
29+
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl);
30+
$redirectUrl .= '#' . $parsedUrl['query'];
31+
}
32+
33+
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);
34+
35+
return $redirectUrl;
36+
}
37+
38+
public function doFoo(int $i)
39+
{
40+
$a = ['a' => $i];
41+
if (rand(0, 1)) {
42+
$a['b'] = $i;
43+
}
44+
45+
if (rand(0,1)) {
46+
$a = ['d' => $i];
47+
}
48+
49+
assertType('array{a: int, b?: int}|array{d: int}', $a);
50+
}
51+
}

tests/PHPStan/Analyser/nsrt/bug-2001.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php
1+
<?php // lint < 8.0
22

33
namespace Bug2001;
44

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug4099Php8;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param array{key: array{inner: mixed}} $arr
13+
*/
14+
function arrayHint(array $arr): void
15+
{
16+
assertType('array{key: array{inner: mixed}}', $arr);
17+
assertNativeType('array', $arr);
18+
19+
if (!array_key_exists('key', $arr)) {
20+
assertType('*NEVER*', $arr);
21+
assertNativeType("array<mixed~'key', mixed>", $arr);
22+
throw new \Exception('no key "key" found.');
23+
}
24+
assertType('array{key: array{inner: mixed}}', $arr);
25+
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
26+
assertType('array{inner: mixed}', $arr['key']);
27+
assertNativeType('mixed', $arr['key']);
28+
29+
if (!array_key_exists('inner', $arr['key'])) {
30+
assertType('*NEVER*', $arr);
31+
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
32+
assertType('*NEVER*', $arr['key']);
33+
assertNativeType("array<mixed~'inner', mixed>", $arr['key']);
34+
throw new \Exception('need key.inner');
35+
}
36+
37+
assertType('array{key: array{inner: mixed}}', $arr);
38+
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
39+
}
40+
41+
}

tests/PHPStan/Analyser/nsrt/bug-4099.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php
1+
<?php // lint < 8.0
22

33
namespace Bug4099;
44

0 commit comments

Comments
 (0)