Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,22 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
}
}

// array_key_first($a) !== null
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought you forgot null !== array_key_first($a) but seems like it works looking at the test.
I'm not familiar with this code to know why at first sight.

Copy link
Contributor Author

@staabm staabm Feb 10, 2026

Choose a reason for hiding this comment

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

we are in resolveNormalizedIdentical, which is invoked in both directions here:

// Normalize to: fn() === expr
if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) {
$specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical(
$rightExpr,
$leftExpr,
), $scope, $context);
} else {
$specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical(
$leftExpr,
$rightExpr,
), $scope, $context);
}

// array_key_last($a) !== null
if (
$unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
&& isset($unwrappedLeftExpr->getArgs()[0])
&& $rightType->isNull()->yes()
Copy link
Contributor

Choose a reason for hiding this comment

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

You're getting escaped mutant here, but if I understand correctly, it should be supported by

if (array_key_first($array) !== $nullOrInt) {
		assertType('list<string>', $array);
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree. I tested it locally and the test starts failling when I manually mutate the code.

) {
$args = $unwrappedLeftExpr->getArgs();
$argType = $scope->getType($args[0]->value);
if ($argType->isArray()->yes()) {
return $this->create($args[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr);
}
}

// preg_match($a) === $b
if (
$context->true()
Expand Down
94 changes: 94 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13546.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
namespace Bug13546;

use function PHPStan\Testing\assertType;

/** @param array<int> $array */
function first(array $array): void
{
if (array_key_first($array) !== null) {
assertType('non-empty-array<int>', $array);
} else {
assertType('array{}', $array);
}
assertType('array<int>', $array);
}

/** @param array<int> $array */
function firstReversed(array $array): void
{
if (null !== array_key_first($array)) {
assertType('non-empty-array<int>', $array);
} else {
assertType('array{}', $array);
}
assertType('array<int>', $array);
}

/** @param array<int> $array */
function last(array $array): void
{
if (array_key_last($array) !== null) {
assertType('non-empty-array<int>', $array);
} else {
assertType('array{}', $array);
}
assertType('array<int>', $array);
}

function maybeArray(array $array, $mixed): void
{
$arrayOrMixed = rand(0, 1) ? $array : $mixed;

if (array_key_last($arrayOrMixed) !== null) {
assertType('mixed', $arrayOrMixed);
} else {
assertType('mixed', $arrayOrMixed);
}
assertType('mixed', $arrayOrMixed);
}

function mixedLast($mixed): void
{
if (is_array($mixed)) {
return;
}

if (array_key_last($mixed) !== null) {
assertType('mixed~array<mixed, mixed>', $mixed);
} else {
assertType('mixed~array<mixed, mixed>', $mixed);
}
assertType('mixed~array<mixed, mixed>', $mixed);
}

/** @param list<string> $array */
function firstInCondition(array $array)
{
if (($key = array_key_first($array)) !== null) {
assertType('list<string>', $array); // could be 'non-empty-list<string>'
return $array[$key];
}
assertType('list<string>', $array);
return null;
}

/** @param list<string> $array */
function maybeNull(array $array, ?int $nullOrInt, ?string $nullOrString): void
{
if (array_key_first($array) !== $nullOrInt) {
assertType('list<string>', $array);
}
if (array_key_first($array) !== $nullOrString) {
assertType('list<string>', $array);
}
assertType('list<string>', $array);

if (array_key_last($array) !== $nullOrInt) {
assertType('list<string>', $array);
}
if (array_key_last($array) !== $nullOrString) {
assertType('list<string>', $array);
}
assertType('list<string>', $array);
}
Loading