Skip to content
23 changes: 23 additions & 0 deletions src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\HasOffsetType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
Expand All @@ -31,6 +32,12 @@ final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTyp

private TypeSpecifier $typeSpecifier;

public function __construct(
private PhpVersion $phpVersion,
)
{
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
Expand Down Expand Up @@ -110,6 +117,22 @@ public function specifyTypes(
new ArrayType(new MixedType(), new MixedType()),
new HasOffsetType($keyType),
);
} elseif ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
$specifiedTypes = $this->typeSpecifier->create(
$array,
new HasOffsetType($keyType),
$context,
$scope,
);

$type = new ArrayType(new MixedType(), new MixedType());
$type = $type->unsetOffset($keyType);
return $specifiedTypes->unionWith($this->typeSpecifier->create(
$array,
$type,
$context->negate(),
$scope,
));
} else {
$type = new HasOffsetType($keyType);
}
Expand Down
12 changes: 6 additions & 6 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,7 @@ public static function dataCondition(): iterable
'$array' => 'non-empty-array',
],
[
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
],
],
[
Expand All @@ -1055,7 +1055,7 @@ public static function dataCondition(): iterable
]),
)),
[
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
],
[
'$array' => 'non-empty-array',
Expand All @@ -1070,7 +1070,7 @@ public static function dataCondition(): iterable
'$array' => 'non-empty-array&hasOffset(\'foo\')',
],
[
'$array' => '~hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('foo')",
],
],
[
Expand All @@ -1088,7 +1088,7 @@ public static function dataCondition(): iterable
'$array' => 'non-empty-array',
],
[
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
],
],
[
Expand All @@ -1103,7 +1103,7 @@ public static function dataCondition(): iterable
]),
)),
[
'$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('bar')|hasOffset('foo')",
],
[
'$array' => 'non-empty-array',
Expand All @@ -1118,7 +1118,7 @@ public static function dataCondition(): iterable
'$array' => 'non-empty-array&hasOffset(\'foo\')',
],
[
'$array' => '~hasOffset(\'foo\')',
'$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array<mixed~'foo', mixed> & ~hasOffset('foo')",
],
],
[
Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.0

declare(strict_types=1);

namespace Bug13270bPhp8;

use function PHPStan\Testing\assertType;

class Test
{
/**
* @param mixed[] $data
* @return mixed[]
*/
public function parseData(array $data): array
{
if (isset($data['price'])) {
assertType('mixed~null', $data['price']);
if (!array_key_exists('priceWithVat', $data['price'])) {
$data['price']['priceWithVat'] = null;
}
assertType("non-empty-array&hasOffsetValue('priceWithVat', mixed)", $data['price']);
if (!array_key_exists('priceWithoutVat', $data['price'])) {
$data['price']['priceWithoutVat'] = null;
}
assertType("non-empty-array&hasOffsetValue('priceWithoutVat', mixed)&hasOffsetValue('priceWithVat', mixed)", $data['price']);
}
return $data;
}
}
4 changes: 3 additions & 1 deletion tests/PHPStan/Analyser/nsrt/bug-13270b.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php // lint < 8.0

declare(strict_types=1);

namespace Bug13270b;

Expand Down
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13301-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php // lint >= 8.0

namespace Bug13301Php8;

use function PHPStan\Testing\assertType;

function doFoo($mixed) {
if (array_key_exists('a', $mixed)) {
assertType("non-empty-array&hasOffset('a')", $mixed);
echo "has-a";
} else {
assertType("array<mixed~'a', mixed>", $mixed);
echo "NO-a";
}
assertType('array', $mixed);
}

function doFooTrue($mixed) {
if (array_key_exists('a', $mixed) === true) {
assertType("non-empty-array&hasOffset('a')", $mixed);
} else {
assertType("array<mixed~'a', mixed>", $mixed);
}
assertType('array', $mixed);
}

function doFooTruethy($mixed) {
if (array_key_exists('a', $mixed) == true) {
assertType("non-empty-array&hasOffset('a')", $mixed);
} else {
assertType("array<mixed~'a', mixed>", $mixed);
}
assertType('array', $mixed);
}

function doFooFalsey($mixed) {
if (array_key_exists('a', $mixed) == 0) {
assertType("array<mixed~'a', mixed>", $mixed);
} else {
assertType("non-empty-array&hasOffset('a')", $mixed);
}
assertType('array', $mixed);
}

function doArray(array $arr) {
if (array_key_exists('a', $arr)) {
assertType("non-empty-array&hasOffset('a')", $arr);
} else {
assertType("array<mixed~'a', mixed>", $arr);
}
assertType('array', $arr);
}
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13301.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php // lint < 8.0

namespace Bug13301Php7;

use function PHPStan\Testing\assertType;

function doFoo($mixed) {
if (array_key_exists('a', $mixed)) {
assertType("non-empty-array&hasOffset('a')", $mixed);
echo "has-a";
} else {
assertType("mixed~hasOffset('a')", $mixed);
echo "NO-a";
}
}
51 changes: 51 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-2001-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php // lint >= 8.0

namespace Bug2001Php8;

use function PHPStan\Testing\assertType;

class HelloWorld
{
public function parseUrl(string $url): string
{
$parsedUrl = parse_url(urldecode($url));
assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl);

if (array_key_exists('host', $parsedUrl)) {
assertType('array{scheme?: string, host: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);
throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.');
}

assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);

$redirectUrl = $parsedUrl['path'];

if (array_key_exists('query', $parsedUrl)) {
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $parsedUrl);
$redirectUrl .= '?' . $parsedUrl['query'];
}

if (array_key_exists('fragment', $parsedUrl)) {
assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl);
$redirectUrl .= '#' . $parsedUrl['query'];
}

assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);

return $redirectUrl;
}

public function doFoo(int $i)
{
$a = ['a' => $i];
if (rand(0, 1)) {
$a['b'] = $i;
}

if (rand(0,1)) {
$a = ['d' => $i];
}

assertType('array{a: int, b?: int}|array{d: int}', $a);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-2001.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php
<?php // lint < 8.0

namespace Bug2001;

Expand Down
41 changes: 41 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-4099-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php // lint >= 8.0

namespace Bug4099Php8;

use function PHPStan\Testing\assertNativeType;
use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param array{key: array{inner: mixed}} $arr
*/
function arrayHint(array $arr): void
{
assertType('array{key: array{inner: mixed}}', $arr);
assertNativeType('array', $arr);

if (!array_key_exists('key', $arr)) {
assertType('*NEVER*', $arr);
assertNativeType("array<mixed~'key', mixed>", $arr);
throw new \Exception('no key "key" found.');
}
assertType('array{key: array{inner: mixed}}', $arr);
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
assertType('array{inner: mixed}', $arr['key']);
assertNativeType('mixed', $arr['key']);

if (!array_key_exists('inner', $arr['key'])) {
assertType('*NEVER*', $arr);
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
assertType('*NEVER*', $arr['key']);
assertNativeType("array<mixed~'inner', mixed>", $arr['key']);
throw new \Exception('need key.inner');
}

assertType('array{key: array{inner: mixed}}', $arr);
assertNativeType('non-empty-array&hasOffset(\'key\')', $arr);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-4099.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php
<?php // lint < 8.0

namespace Bug4099;

Expand Down
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace ConditionalVarsPhp8;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/** @param array<mixed> $innerHits */
public function conditionalVarInTernary(array $innerHits): void
{
if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
assertType('non-empty-array', $innerHits);
$x = array_key_exists('nearest_premise', $innerHits)
? assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits)
: assertType("non-empty-array<mixed~'nearest_premise', mixed>", $innerHits);

assertType('non-empty-array', $innerHits);
}
assertType('array', $innerHits);
}

/** @param array<mixed> $innerHits */
public function conditionalVarInIf(array $innerHits): void
{
if (array_key_exists('nearest_premise', $innerHits) || array_key_exists('matching_premises', $innerHits)) {
assertType('non-empty-array', $innerHits);
if (array_key_exists('nearest_premise', $innerHits)) {
assertType("non-empty-array&hasOffset('nearest_premise')", $innerHits);
} else {
assertType("non-empty-array<mixed~'nearest_premise', mixed>", $innerHits);
}

assertType('non-empty-array', $innerHits);
}
assertType('array', $innerHits);
}
}
4 changes: 3 additions & 1 deletion tests/PHPStan/Analyser/nsrt/conditional-vars.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types = 1);
<?php // lint < 8.0

declare(strict_types = 1);

namespace ConditionalVars;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,4 +1113,13 @@ public function testBug12805(): void
$this->analyse([__DIR__ . '/data/bug-12805.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug6209(): void
{
$this->checkExplicitMixed = true;
$this->checkImplicitMixed = false;

$this->analyse([__DIR__ . '/data/bug-6209.php'], []);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-6209.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Bug6209;

/**
* @param mixed[] $values
* @return mixed[]
*/
function apply(array $values): array
{
if (!array_key_exists('key', $values)) {
$values['key'][] = 'any';
}

return $values;
}
Loading