diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index c9de826666..f5b7b31a3b 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,13 @@ public function specifyTypes( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), ); + } elseif ( + $this->phpVersion->throwsValueErrorForInternalFunctions() + && !$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-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, 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 69d429d8bd..39cc52ff2a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2001.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2001.php @@ -1,4 +1,4 @@ -