From 660abc9a24a243e408987dd3c0369620888a6947 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 09:56:52 +0200 Subject: [PATCH 01/14] `!array_key_exists()` should imply `array` for PHP8+ --- ...yExistsFunctionTypeSpecifyingExtension.php | 15 ++++++++++ .../PHPStan/Analyser/nsrt/bug-13270b-php8.php | 30 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-13270b.php | 4 ++- .../PHPStan/Analyser/nsrt/bug-13301-php8.php | 15 ++++++++++ tests/PHPStan/Analyser/nsrt/bug-13301.php | 15 ++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13301-php8.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13301.php diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index c9de826666..7edfbe25cf 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -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; @@ -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; @@ -110,6 +117,14 @@ public function specifyTypes( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), ); + } elseif ( + $this->phpVersion->throwsValueErrorForInternalFunctions() + && $arrayType instanceof MixedType + ) { + $type = TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + ); + $context = $context->negate(); } else { $type = new HasOffsetType($keyType); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php new file mode 100644 index 0000000000..31fe8474c4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php @@ -0,0 +1,30 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug13270b; + +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; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270b.php b/tests/PHPStan/Analyser/nsrt/bug-13270b.php index a921ed1ddb..ad79c8a880 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13270b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b.php @@ -1,4 +1,6 @@ -= 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); // could be array~hasOffset('a') after arrays got subtractable + echo "NO-a"; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301.php b/tests/PHPStan/Analyser/nsrt/bug-13301.php new file mode 100644 index 0000000000..738195b8f6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13301.php @@ -0,0 +1,15 @@ + Date: Tue, 7 Oct 2025 09:59:41 +0200 Subject: [PATCH 02/14] Update bug-13301-php8.php --- tests/PHPStan/Analyser/nsrt/bug-13301-php8.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php index 49bea0e035..4075d937b7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php @@ -12,4 +12,16 @@ function doFoo($mixed) { assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable echo "NO-a"; } + assertType('array', $mixed); +} + +function doArray(array $arr) { + if (array_key_exists('a', $arr)) { + assertType("non-empty-array&hasOffset('a')", $arr); + echo "has-a"; + } else { + assertType('array', $arr); + echo "NO-a"; + } + assertType('array', $arr); } From 4a5dcdced497d308a457c5d99ca3883a4a280342 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 10:00:32 +0200 Subject: [PATCH 03/14] Update bug-13270b-php8.php --- tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php index 31fe8474c4..ecab6997b8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Bug13270b; +namespace Bug13270bPhp8; use function PHPStan\Testing\assertType; From e592ba56102e81a4949f4b6db2206410a0b6b5a3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 11:20:57 +0200 Subject: [PATCH 04/14] test different context variants --- .../PHPStan/Analyser/nsrt/bug-13301-php8.php | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php index 4075d937b7..f8b481210f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php @@ -15,13 +15,38 @@ function doFoo($mixed) { assertType('array', $mixed); } +function doFooTrue($mixed) { + if (array_key_exists('a', $mixed) === true) { + assertType("non-empty-array&hasOffset('a')", $mixed); + } else { + assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable + } + assertType('array', $mixed); +} + +function doFooTruethy($mixed) { + if (array_key_exists('a', $mixed) == true) { + assertType("non-empty-array&hasOffset('a')", $mixed); + } else { + assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable + } + assertType('array', $mixed); +} + +function doFooFalsey($mixed) { + if (array_key_exists('a', $mixed) == 0) { + assertType("array", $mixed); + } else { + assertType("non-empty-array&hasOffset('a')", $mixed); // could be array~hasOffset('a') after arrays got subtractable + } + assertType('array', $mixed); +} + function doArray(array $arr) { if (array_key_exists('a', $arr)) { assertType("non-empty-array&hasOffset('a')", $arr); - echo "has-a"; } else { assertType('array', $arr); - echo "NO-a"; } assertType('array', $arr); } From 787be0e02d314202673f6cd77eecf5179bd75d8f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 15:17:04 +0200 Subject: [PATCH 05/14] get rid of instanceof Mixed --- .../Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php | 6 ++---- tests/PHPStan/Analyser/nsrt/bug-2001.php | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 7edfbe25cf..b8c2baebb4 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -119,11 +119,9 @@ public function specifyTypes( ); } elseif ( $this->phpVersion->throwsValueErrorForInternalFunctions() - && $arrayType instanceof MixedType + && !$arrayType->isArray()->yes() ) { - $type = TypeCombinator::intersect( - new ArrayType(new MixedType(), new MixedType()), - ); + $type = new ArrayType(new MixedType(), new MixedType()); $context = $context->negate(); } else { $type = new HasOffsetType($keyType); diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001.php b/tests/PHPStan/Analyser/nsrt/bug-2001.php index 69d429d8bd..afc7e9d976 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2001.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -16,21 +16,21 @@ public function parseUrl(string $url): string 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}|false', $parsedUrl); + assertType('array{scheme?: string, host?: 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); + assertType('array{scheme?: string, host?: 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); + assertType('array{scheme?: string, host?: 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}|false', $parsedUrl); + assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); return $redirectUrl; } From 0e599f813e3828608ffd3a712e0d96a1265b6849 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 15:30:33 +0200 Subject: [PATCH 06/14] fix php7 build --- tests/PHPStan/Analyser/nsrt/bug-2001-php8.php | 51 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-2001.php | 10 ++-- 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-2001-php8.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php b/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php new file mode 100644 index 0000000000..f346486d9c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php @@ -0,0 +1,51 @@ += 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, host?: 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, host?: 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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl); + $redirectUrl .= '#' . $parsedUrl['query']; + } + + assertType('array{scheme?: string, host?: 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); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001.php b/tests/PHPStan/Analyser/nsrt/bug-2001.php index afc7e9d976..39cc52ff2a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2001.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -1,4 +1,4 @@ -, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); $redirectUrl = $parsedUrl['path']; if (array_key_exists('query', $parsedUrl)) { - assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); return $redirectUrl; } From fe0a4031bacfee613a4df2963385ce4f3844e9ad Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 7 Oct 2025 15:35:58 +0200 Subject: [PATCH 07/14] fix wrong comment --- tests/PHPStan/Analyser/nsrt/bug-13301-php8.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php index f8b481210f..ff420724a2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php @@ -37,7 +37,7 @@ function doFooFalsey($mixed) { if (array_key_exists('a', $mixed) == 0) { assertType("array", $mixed); } else { - assertType("non-empty-array&hasOffset('a')", $mixed); // could be array~hasOffset('a') after arrays got subtractable + assertType("non-empty-array&hasOffset('a')", $mixed); } assertType('array', $mixed); } From 23f6d8bf9248e7886b05949c76674fd8e12e13d5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 8 Oct 2025 06:42:35 +0200 Subject: [PATCH 08/14] fix --- .../Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php | 1 + tests/PHPStan/Analyser/nsrt/bug-13301-php8.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index b8c2baebb4..f5b7b31a3b 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -122,6 +122,7 @@ public function specifyTypes( && !$arrayType->isArray()->yes() ) { $type = new ArrayType(new MixedType(), new MixedType()); + $type = $type->unsetOffset($keyType); $context = $context->negate(); } else { $type = new HasOffsetType($keyType); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php index ff420724a2..ee1eb3428e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php @@ -9,7 +9,7 @@ function doFoo($mixed) { assertType("non-empty-array&hasOffset('a')", $mixed); echo "has-a"; } else { - assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable + assertType("array", $mixed); echo "NO-a"; } assertType('array', $mixed); @@ -19,7 +19,7 @@ function doFooTrue($mixed) { if (array_key_exists('a', $mixed) === true) { assertType("non-empty-array&hasOffset('a')", $mixed); } else { - assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable + assertType("array", $mixed); } assertType('array', $mixed); } @@ -28,14 +28,14 @@ function doFooTruethy($mixed) { if (array_key_exists('a', $mixed) == true) { assertType("non-empty-array&hasOffset('a')", $mixed); } else { - assertType('array', $mixed); // could be array~hasOffset('a') after arrays got subtractable + assertType("array", $mixed); } assertType('array', $mixed); } function doFooFalsey($mixed) { if (array_key_exists('a', $mixed) == 0) { - assertType("array", $mixed); + assertType("array", $mixed); } else { assertType("non-empty-array&hasOffset('a')", $mixed); } From 6b41cf43c919889f02589ad017badc48eadf303b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 12 Oct 2025 09:11:21 +0200 Subject: [PATCH 09/14] more precise array shape --- ...rayKeyExistsFunctionTypeSpecifyingExtension.php | 14 +++++++++++++- tests/PHPStan/Analyser/nsrt/bug-2001-php8.php | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index f5b7b31a3b..d35abbb307 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -121,9 +121,21 @@ public function specifyTypes( $this->phpVersion->throwsValueErrorForInternalFunctions() && !$arrayType->isArray()->yes() ) { + $specifiedTypes = $this->typeSpecifier->create( + $array, + new HasOffsetType($keyType), + $context, + $scope, + ); + $type = new ArrayType(new MixedType(), new MixedType()); $type = $type->unsetOffset($keyType); - $context = $context->negate(); + return $specifiedTypes->unionWith($this->typeSpecifier->create( + $array, + $type, + $context->negate(), + $scope, + )); } else { $type = new HasOffsetType($keyType); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php b/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php index f346486d9c..4b932dfe37 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2001-php8.php @@ -16,21 +16,21 @@ public function parseUrl(string $url): string throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.'); } - assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); + 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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); + assertType('array{scheme?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); return $redirectUrl; } From 458d36b47d880785c559d633a2917b06bc368197 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 12 Oct 2025 09:26:29 +0200 Subject: [PATCH 10/14] narrow in more cases --- ...yExistsFunctionTypeSpecifyingExtension.php | 5 +-- tests/PHPStan/Analyser/TypeSpecifierTest.php | 12 +++--- .../PHPStan/Analyser/nsrt/bug-13301-php8.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-4099-php8.php | 41 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-4099.php | 2 +- .../Analyser/nsrt/conditional-vars-php8.php | 40 ++++++++++++++++++ .../Analyser/nsrt/conditional-vars.php | 4 +- 7 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-4099-php8.php create mode 100644 tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index d35abbb307..127bc326da 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -117,10 +117,7 @@ public function specifyTypes( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), ); - } elseif ( - $this->phpVersion->throwsValueErrorForInternalFunctions() - && !$arrayType->isArray()->yes() - ) { + } elseif ($this->phpVersion->throwsValueErrorForInternalFunctions()) { $specifiedTypes = $this->typeSpecifier->create( $array, new HasOffsetType($keyType), diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 62b5188400..8d0033dac0 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -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 & ~hasOffset('bar')|hasOffset('foo')", ], ], [ @@ -1055,7 +1055,7 @@ public static function dataCondition(): iterable ]), )), [ - '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + '$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array & ~hasOffset('bar')|hasOffset('foo')", ], [ '$array' => 'non-empty-array', @@ -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 & ~hasOffset('foo')", ], ], [ @@ -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 & ~hasOffset('bar')|hasOffset('foo')", ], ], [ @@ -1103,7 +1103,7 @@ public static function dataCondition(): iterable ]), )), [ - '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', + '$array' => "array & ~hasOffset('bar')|hasOffset('foo')", ], [ '$array' => 'non-empty-array', @@ -1118,7 +1118,7 @@ public static function dataCondition(): iterable '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ - '$array' => '~hasOffset(\'foo\')', + '$array' => "array & ~hasOffset('foo')", ], ], [ diff --git a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php index ee1eb3428e..83ac44864b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13301-php8.php @@ -46,7 +46,7 @@ function doArray(array $arr) { if (array_key_exists('a', $arr)) { assertType("non-empty-array&hasOffset('a')", $arr); } else { - assertType('array', $arr); + assertType("array", $arr); } assertType('array', $arr); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099-php8.php b/tests/PHPStan/Analyser/nsrt/bug-4099-php8.php new file mode 100644 index 0000000000..e30576e4b6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4099-php8.php @@ -0,0 +1,41 @@ += 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", $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", $arr['key']); + throw new \Exception('need key.inner'); + } + + assertType('array{key: array{inner: mixed}}', $arr); + assertNativeType('non-empty-array&hasOffset(\'key\')', $arr); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4099.php b/tests/PHPStan/Analyser/nsrt/bug-4099.php index 5e5eb30ca2..137dd5bae9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4099.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4099.php @@ -1,4 +1,4 @@ -= 8.0 + +declare(strict_types = 1); + +namespace ConditionalVars; + +use function PHPStan\Testing\assertType; + +class HelloWorld +{ + /** @param array $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", $innerHits); + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } + + /** @param array $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", $innerHits); + } + + assertType('non-empty-array', $innerHits); + } + assertType('array', $innerHits); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars.php b/tests/PHPStan/Analyser/nsrt/conditional-vars.php index 568c6a8b7f..f67cdc11b4 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-vars.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars.php @@ -1,4 +1,6 @@ - Date: Sun, 12 Oct 2025 09:29:13 +0200 Subject: [PATCH 11/14] fix name collision in test --- tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php b/tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php index 92a7c8b877..ca8dacc0a2 100644 --- a/tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php +++ b/tests/PHPStan/Analyser/nsrt/conditional-vars-php8.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace ConditionalVars; +namespace ConditionalVarsPhp8; use function PHPStan\Testing\assertType; From c8b65f7005c2d7f2946a05ababc44c34f7fa54d0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 12 Oct 2025 09:42:09 +0200 Subject: [PATCH 12/14] fix php7 --- tests/PHPStan/Analyser/TypeSpecifierTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 8d0033dac0..9edca89ab9 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1103,7 +1103,7 @@ public static function dataCondition(): iterable ]), )), [ - '$array' => "array & ~hasOffset('bar')|hasOffset('foo')", + '$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'bar\')|hasOffset(\'foo\')' : "array & ~hasOffset('bar')|hasOffset('foo')", ], [ '$array' => 'non-empty-array', @@ -1118,7 +1118,7 @@ public static function dataCondition(): iterable '$array' => 'non-empty-array&hasOffset(\'foo\')', ], [ - '$array' => "array & ~hasOffset('foo')", + '$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array & ~hasOffset('foo')", ], ], [ From efb0ab4d13004f7625dc53820f9ccd9a42b20794 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 12 Oct 2025 19:29:17 +0200 Subject: [PATCH 13/14] added regression test --- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ tests/PHPStan/Rules/Arrays/data/bug-6209.php | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-6209.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 471a1a1b01..aec4f42541 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1113,4 +1113,12 @@ public function testBug12805(): void $this->analyse([__DIR__ . '/data/bug-12805.php'], []); } + public function testBug6209(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = false; + + $this->analyse([__DIR__ . '/data/bug-6209.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6209.php b/tests/PHPStan/Rules/Arrays/data/bug-6209.php new file mode 100644 index 0000000000..b1650d0867 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6209.php @@ -0,0 +1,16 @@ + Date: Sun, 12 Oct 2025 19:36:33 +0200 Subject: [PATCH 14/14] fix build --- .../Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index aec4f42541..16f361cb40 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1113,6 +1113,7 @@ public function testBug12805(): void $this->analyse([__DIR__ . '/data/bug-12805.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug6209(): void { $this->checkExplicitMixed = true;