Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 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,12 @@ 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());
$context = $context->negate();
} else {
$type = new HasOffsetType($keyType);
}
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); // could be array~hasOffset('a') after arrays got subtractable
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); // 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);
}
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);
}
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, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl);
Copy link
Contributor Author

@staabm staabm Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case we got more preicse, because we no longer have a union array{...}|false but just the plain array-shape.
but on the other hand we no longer know that the 'host' offset cannot exist at this point.

I tried several variants, but wasn't successfull in combining the specified types in a way which turns the mixed into array while also removing the offset.


$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);
}
}
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
Loading