From 0ea52a0eff6cf2c0f3b376e4800a56d5ef110e6b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 10:52:37 +0100 Subject: [PATCH 1/8] Infer non-empty-array after array_key_first/last() --- src/Analyser/TypeSpecifier.php | 15 +++++++++ tests/PHPStan/Analyser/nsrt/bug-13546.php | 37 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13546.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3b2b1e0f11..4d4c8b4edc 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2383,6 +2383,21 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope } } + // array_key_first($a) !== null + // 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() + ) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isArray()->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + } + } + // preg_match($a) === $b if ( $context->true() diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php new file mode 100644 index 0000000000..47beb71390 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -0,0 +1,37 @@ + $array */ +function first(array $array): void +{ + if (array_key_first($array) !== null) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} + +/** @param array $array */ +function firstReversed(array $array): void +{ + if (null !== array_key_first($array)) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} + +/** @param array $array */ +function last(array $array): void +{ + if (array_key_last($array) !== null) { + assertType('non-empty-array', $array); + } else { + assertType('array{}', $array); + } + assertType('array', $array); +} From d1aece6a5acb21373fd174c4c2769d4d1746339f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 10:56:15 +0100 Subject: [PATCH 2/8] Update bug-13546.php --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 47beb71390..17d221d495 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -35,3 +35,29 @@ function last(array $array): void } assertType('array', $array); } + +function maybeArray(array $array, mixed $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 $mixed): void +{ + if (is_array($mixed)) { + return; + } + + if (array_key_last($mixed) !== null) { + assertType('mixed~array', $mixed); + } else { + assertType('mixed~array', $mixed); + } + assertType('mixed~array', $mixed); +} From 37ea055f46f871454d008d7f37c9b9a6a82ebc52 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:30:54 +0100 Subject: [PATCH 3/8] add test --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 17d221d495..588e90d162 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -61,3 +61,14 @@ function mixedLast(mixed $mixed): void } assertType('mixed~array', $mixed); } + +/** @param list $array */ +function firstInCondition(array $array): mixed +{ + if (($key = array_key_first($array)) !== null) { + assertType('list', $array); // could be 'non-empty-list' + return $array[$key]; + } + assertType('list', $array); + return null; +} From 2505ba44b8f00fe725da5f71935f6e83dcb1381f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:33:51 +0100 Subject: [PATCH 4/8] test maybe null --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 588e90d162..c390b717d0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -72,3 +72,15 @@ function firstInCondition(array $array): mixed assertType('list', $array); return null; } + +/** @param list $array */ +function maybeNull(array $array, ?int $nullOrInt, ?string $nullOrString): void +{ + if (array_key_first($array) !== $nullOrInt) { + assertType('list', $array); + } + if (array_key_first($array) !== $nullOrString) { + assertType('list', $array); + } + assertType('list', $array); +} From 1427f89df27e00d7b95f34780a637a0f5b3c985c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:37:54 +0100 Subject: [PATCH 5/8] fix php 7.4 compat --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index c390b717d0..cfa4dd9aba 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -63,7 +63,7 @@ function mixedLast(mixed $mixed): void } /** @param list $array */ -function firstInCondition(array $array): mixed +function firstInCondition(array $array) { if (($key = array_key_first($array)) !== null) { assertType('list', $array); // could be 'non-empty-list' From 7d4c4dcad48f0dd1a56968eedc7fe28b00a184aa Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 8 Nov 2025 15:52:51 +0100 Subject: [PATCH 6/8] Update bug-13546.php --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index cfa4dd9aba..63eb5ee889 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -36,7 +36,7 @@ function last(array $array): void assertType('array', $array); } -function maybeArray(array $array, mixed $mixed): void +function maybeArray(array $array, $mixed): void { $arrayOrMixed = rand(0, 1) ? $array : $mixed; @@ -48,7 +48,7 @@ function maybeArray(array $array, mixed $mixed): void assertType('mixed', $arrayOrMixed); } -function mixedLast(mixed $mixed): void +function mixedLast($mixed): void { if (is_array($mixed)) { return; From c8f2b3b6d689e4af6d50543529f0b406c5f76b96 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 07:29:50 +0100 Subject: [PATCH 7/8] add test --- tests/PHPStan/Analyser/nsrt/bug-13546.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13546.php b/tests/PHPStan/Analyser/nsrt/bug-13546.php index 63eb5ee889..465689b211 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13546.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13546.php @@ -83,4 +83,12 @@ function maybeNull(array $array, ?int $nullOrInt, ?string $nullOrString): void assertType('list', $array); } assertType('list', $array); + + if (array_key_last($array) !== $nullOrInt) { + assertType('list', $array); + } + if (array_key_last($array) !== $nullOrString) { + assertType('list', $array); + } + assertType('list', $array); } From 3532071f2c3ccd4c371538a504bde6056d1a63ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 07:31:15 +0100 Subject: [PATCH 8/8] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4d4c8b4edc..587818cbb3 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2392,9 +2392,10 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope && isset($unwrappedLeftExpr->getArgs()[0]) && $rightType->isNull()->yes() ) { - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $args = $unwrappedLeftExpr->getArgs(); + $argType = $scope->getType($args[0]->value); if ($argType->isArray()->yes()) { - return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); + return $this->create($args[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); } }