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
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
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,13 @@ public function testBug14542(): void
$this->assertNoErrors($errors);
}

public function testBug14550(): void
{
// crash
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14550.php');
$this->assertNotEmpty($errors);
}

/**
* @param string[]|null $allAnalysedFiles
* @return list<Error>
Expand Down
130 changes: 130 additions & 0 deletions tests/PHPStan/Analyser/data/bug-14550.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php // lint >= 8.1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Turn this into a AnalyserIntegrationTest as we are covering crash bugs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Converted the bug-14550 test from an NSRT test (tests/PHPStan/Analyser/nsrt/bug-14550.php) to an AnalyserIntegrationTest (tests/PHPStan/Analyser/data/bug-14550.php), since this is a crash bug. The test data file was simplified to remove assertType() calls (not needed for crash prevention), and a testBug14550() method was added to AnalyserIntegrationTest that verifies the analysis completes without crashing. All 11,941 tests pass and make phpstan reports no errors.


declare(strict_types = 1);

namespace Bug14550;
Comment thread
staabm marked this conversation as resolved.

function testArrayKeyFirstAssign(): void
{
$fn = array_key_first(...);
}

function testArrayKeyLastAssign(): void
{
$fn = array_key_last(...);
}

function testArrayRandAssign(): void
{
$fn = array_rand(...);
}

function testCountMinusOneAssign(): void
{
$idx = count(...) - 1;
}

function testArraySearchInCondition(): void
{
if ($key = array_search(...)) {
}
}

function testCountInComparisons(): void
{
if (count(...) < 1) {}
if (0 < count(...)) {}
}

function testSizeofInComparisons(): void
{
if (sizeof(...) < 1) {}
if (0 < sizeof(...)) {}
}

function testCountMinusOneInComparison(): void
{
$i = 0;
if ($i < count(...) - 1) {}
}

function testStrlenInComparisons(): void
{
if (strlen(...) < 1) {}
if (0 < strlen(...)) {}
}

function testMbStrlenInComparisons(): void
{
if (mb_strlen(...) < 1) {}
if (0 < mb_strlen(...)) {}
}

function testPregMatchInComparisons(): void
{
if (preg_match(...) < 1) {}
if (0 < preg_match(...)) {}
}

function testCountIdentical(): void
{
if (count(...) === 0) {}
}

function testStrlenIdentical(): void
{
if (strlen(...) === 0) {}
if (mb_strlen(...) === 0) {}
}

function testArrayKeyFirstNullComparison(): void
{
if (array_key_first(...) !== null) {}
if (array_key_last(...) !== null) {}
}

function testGetClassIdentical(): void
{
if (get_class(...) === 'stdClass') {}
if (get_debug_type(...) === 'string') {}
}

function testStringFuncIdentical(): void
{
if (strtolower(...) === 'test') {}
}

function testGettypeEquality(): void
{
if (gettype(...) === 'string') {}
if (gettype(...) == 'string') {}
}

function testGetClassEquality(): void
{
if (get_class(...) == 'stdClass') {}
if (get_debug_type(...) == 'string') {}
}

function testGetParentClassEquality(): void
{
if (get_parent_class(...) === 'stdClass') {}
}

function testTrimEquality(): void
{
if (trim(...) !== '') {}
if (ltrim(...) !== '') {}
if (rtrim(...) !== '') {}
}

function testArrayKeysInForeach(): void
{
foreach (array_keys(...) as $key) {}
}

function testCountInForLoop(): void
{
for ($i = 0; $i < count(...); $i++) {}
for ($i = 0; count(...) > $i; $i++) {}
}
Loading