Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4141,6 +4141,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto
if (
$stmt->expr instanceof FuncCall
&& $stmt->expr->name instanceof Name
&& !$stmt->expr->isFirstClassCallable()
&& $stmt->expr->name->toLowerString() === 'array_keys'
&& $stmt->valueVar instanceof Variable
) {
Expand Down Expand Up @@ -4816,6 +4817,7 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
&& $lastCondExpr->left instanceof Variable
&& $lastCondExpr->right instanceof FuncCall
&& $lastCondExpr->right->name instanceof Name
&& !$lastCondExpr->right->isFirstClassCallable()
&& in_array($lastCondExpr->right->name->toLowerString(), ['count', 'sizeof'], true)
&& count($lastCondExpr->right->getArgs()) > 0
&& $lastCondExpr->right->getArgs()[0]->value instanceof Variable
Expand All @@ -4840,6 +4842,7 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
&& $lastCondExpr->right instanceof Variable
&& $lastCondExpr->left instanceof FuncCall
&& $lastCondExpr->left->name instanceof Name
&& !$lastCondExpr->left->isFirstClassCallable()
&& in_array($lastCondExpr->left->name->toLowerString(), ['count', 'sizeof'], true)
&& count($lastCondExpr->left->getArgs()) > 0
&& $lastCondExpr->left->getArgs()[0]->value instanceof Variable
Expand Down
21 changes: 20 additions & 1 deletion src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ public function specifyTypesInCondition(
if (
$expr->left instanceof FuncCall
&& $expr->left->name instanceof Name
&& !$expr->left->isFirstClassCallable()
&& in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true)
&& count($expr->left->getArgs()) >= 1
&& (
Expand Down Expand Up @@ -275,6 +276,7 @@ public function specifyTypesInCondition(
!$context->null()
&& $expr->right instanceof FuncCall
&& $expr->right->name instanceof Name
&& !$expr->right->isFirstClassCallable()
&& in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
&& count($expr->right->getArgs()) >= 1
&& $leftType->isInteger()->yes()
Expand Down Expand Up @@ -378,6 +380,7 @@ public function specifyTypesInCondition(
&& $expr->right instanceof Expr\BinaryOp\Minus
&& $expr->right->left instanceof FuncCall
&& $expr->right->left->name instanceof Name
&& !$expr->right->left->isFirstClassCallable()
&& in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true)
&& count($expr->right->left->getArgs()) >= 1
// constant offsets are handled via HasOffsetType/HasOffsetValueType
Expand All @@ -404,6 +407,7 @@ public function specifyTypesInCondition(
!$context->null()
&& $expr->right instanceof FuncCall
&& $expr->right->name instanceof Name
&& !$expr->right->isFirstClassCallable()
&& in_array(strtolower((string) $expr->right->name), ['preg_match'], true)
&& count($expr->right->getArgs()) >= 3
&& (
Expand All @@ -421,6 +425,7 @@ public function specifyTypesInCondition(
!$context->null()
&& $expr->right instanceof FuncCall
&& $expr->right->name instanceof Name
&& !$expr->right->isFirstClassCallable()
&& in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true)
Comment thread
staabm marked this conversation as resolved.
&& count($expr->right->getArgs()) === 1
&& $leftType->isInteger()->yes()
Expand Down Expand Up @@ -825,6 +830,7 @@ public function specifyTypesInCondition(
if (
$expr->expr instanceof FuncCall
&& $expr->expr->name instanceof Name
&& !$expr->expr->isFirstClassCallable()
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
&& count($expr->expr->getArgs()) >= 1
) {
Expand Down Expand Up @@ -881,6 +887,7 @@ public function specifyTypesInCondition(
if (
$expr->expr instanceof FuncCall
&& $expr->expr->name instanceof Name
&& !$expr->expr->isFirstClassCallable()
&& in_array($expr->expr->name->toLowerString(), ['array_rand'], true)
&& count($expr->expr->getArgs()) >= 1
) {
Expand Down Expand Up @@ -911,6 +918,7 @@ public function specifyTypesInCondition(
$expr->expr instanceof Expr\BinaryOp\Minus
&& $expr->expr->left instanceof FuncCall
&& $expr->expr->left->name instanceof Name
&& !$expr->expr->left->isFirstClassCallable()
&& $expr->expr->right instanceof Node\Scalar\Int_
&& $expr->expr->right->value === 1
&& in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
Expand Down Expand Up @@ -938,6 +946,7 @@ public function specifyTypesInCondition(
if (
$expr->expr instanceof FuncCall
&& $expr->expr->name instanceof Name
&& !$expr->expr->isFirstClassCallable()
&& $expr->expr->name->toLowerString() === 'array_search'
&& count($expr->expr->getArgs()) >= 2
) {
Expand Down Expand Up @@ -1596,6 +1605,7 @@ private function specifyTypesForConstantStringBinaryExpression(
if (
$exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& !$exprNode->isFirstClassCallable()
&& strtolower($exprNode->name->toString()) === 'gettype'
&& isset($exprNode->getArgs()[0])
) {
Expand Down Expand Up @@ -1636,6 +1646,7 @@ private function specifyTypesForConstantStringBinaryExpression(
$context->true()
&& $exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& !$exprNode->isFirstClassCallable()
&& strtolower((string) $exprNode->name) === 'get_parent_class'
&& isset($exprNode->getArgs()[0])
) {
Expand Down Expand Up @@ -1673,6 +1684,7 @@ private function specifyTypesForConstantStringBinaryExpression(
$context->false()
&& $exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& !$exprNode->isFirstClassCallable()
&& in_array(strtolower((string) $exprNode->name), [
Comment thread
staabm marked this conversation as resolved.
'trim', 'ltrim', 'rtrim', 'chop',
'mb_trim', 'mb_ltrim', 'mb_rtrim',
Expand Down Expand Up @@ -2750,6 +2762,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
if (
$exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& !$exprNode->isFirstClassCallable()
&& in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true)
Comment thread
staabm marked this conversation as resolved.
&& isset($exprNode->getArgs()[0])
&& $constantType->isString()->yes()
Expand Down Expand Up @@ -2897,6 +2910,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
if (
!$context->null()
&& $unwrappedLeftExpr instanceof FuncCall
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& count($unwrappedLeftExpr->getArgs()) >= 1
&& $unwrappedLeftExpr->name instanceof Name
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
Expand All @@ -2907,6 +2921,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
$context->true()
&& $unwrappedRightExpr instanceof FuncCall
&& $unwrappedRightExpr->name instanceof Name
&& !$unwrappedRightExpr->isFirstClassCallable()
&& in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true)
&& count($unwrappedRightExpr->getArgs()) >= 1
) {
Expand Down Expand Up @@ -2981,9 +2996,10 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
if (
!$context->null()
&& $unwrappedLeftExpr instanceof FuncCall
&& count($unwrappedLeftExpr->getArgs()) === 1
&& $unwrappedLeftExpr->name instanceof Name
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
&& count($unwrappedLeftExpr->getArgs()) === 1
&& $rightType->isInteger()->yes()
) {
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
Expand Down Expand Up @@ -3019,6 +3035,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
if (
$unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
&& isset($unwrappedLeftExpr->getArgs()[0])
&& $rightType->isNull()->yes()
Expand Down Expand Up @@ -3050,6 +3067,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
$context->true()
&& $unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
&& isset($unwrappedLeftExpr->getArgs()[0])
) {
Expand All @@ -3075,6 +3093,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
$context->truthy()
&& $unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& !$unwrappedLeftExpr->isFirstClassCallable()
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), [
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',
Expand Down
126 changes: 126 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14550.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14550;

use function PHPStan\Testing\assertType;

/**
* @param list<string> $list
*/
function crashArrayKeyFirst(array $list): void
{
$fn = array_key_first(...);
assertType('Closure(array): (int|string|null)', $fn);
}

/**
* @param list<string> $list
*/
function crashArrayKeyLast(array $list): void
{
$fn = array_key_last(...);
assertType('Closure(array): (int|string|null)', $fn);
}

/**
* @param list<string> $list
*/
function crashArrayRand(array $list): void
{
$fn = array_rand(...);
assertType('(Closure(non-empty-array): (int|string))|(Closure(non-empty-array, int<1, max>): (array<int, int|string>|int|string))', $fn);
}

/**
* @param list<string> $list
*/
function crashArraySearch(array $list, string $s): void
{
$fn = array_search(...);
assertType('Closure(mixed, array, bool=): (int|string|false)', $fn);
}

function testStrlen(): void
{
$fn = strlen(...);
assertType('Closure(string): int<0, max>', $fn);
}

function testMbStrlen(): void
{
$fn = mb_strlen(...);
assertType('Closure(string, string|null=): int<0, max>', $fn);
}

function testCount(): void
{
$fn = count(...);
assertType('Closure(array|Countable, 0|1=): int<0, max>', $fn);
}

function testSizeof(): void
{
$fn = sizeof(...);
assertType('Closure(array|Countable, int=): int', $fn);
}

function testPregMatch(): void
{
$fn = preg_match(...);
assertType('Closure(string, string, array<string>|null=, TFlags=, int=): (0|1|false)', $fn);
}

function testGettype(): void
{
$fn = gettype(...);
assertType('Closure(mixed): string', $fn);
}

function testGetClass(): void
{
$fn = get_class(...);
assertType('Closure(object=): class-string', $fn);
}

function testGetDebugType(): void
{
$fn = get_debug_type(...);
assertType('Closure(mixed): string', $fn);
}

function testGetParentClass(): void
{
$fn = get_parent_class(...);
assertType('Closure(object|string=): (class-string|false)', $fn);
}

function testTrim(): void
{
$fn = trim(...);
assertType('Closure(string, string=): string', $fn);
}

function testLtrim(): void
{
$fn = ltrim(...);
assertType('Closure(string, string=): string', $fn);
}

function testRtrim(): void
{
$fn = rtrim(...);
assertType('Closure(string, string=): string', $fn);
}

/**
* @param list<string> $list
*/
function testArrayKeysInForeach(array $list): void
{
foreach (array_keys(...) as $key) {
}
$fn = array_keys(...);
assertType('Closure(array, mixed=, bool=): list<int|string>', $fn);
}
Loading