diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index c9de826666..127bc326da 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,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); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 62b5188400..9edca89ab9 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' => 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' => '~hasOffset(\'foo\')', + '$array' => PHP_VERSION_ID < 80000 ? '~hasOffset(\'foo\')' : "array & ~hasOffset('foo')", ], ], [ 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..ecab6997b8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13270b-php8.php @@ -0,0 +1,30 @@ += 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; + } +} 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); + 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); + } + assertType('array', $mixed); +} + +function doFooTruethy($mixed) { + if (array_key_exists('a', $mixed) == true) { + assertType("non-empty-array&hasOffset('a')", $mixed); + } else { + assertType("array", $mixed); + } + assertType('array', $mixed); +} + +function doFooFalsey($mixed) { + if (array_key_exists('a', $mixed) == 0) { + assertType("array", $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", $arr); + } + assertType('array', $arr); +} 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 @@ += 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); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-2001.php b/tests/PHPStan/Analyser/nsrt/bug-2001.php index 69d429d8bd..39cc52ff2a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2001.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -1,4 +1,4 @@ -= 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 ConditionalVarsPhp8; + +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 @@ -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'], []); + } + } 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 @@ +