diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5090d04402..cbed5105ae 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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 ) { @@ -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 @@ -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 diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 24903779b3..1eacf25dd4 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -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 && ( @@ -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() @@ -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 @@ -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 && ( @@ -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) && count($expr->right->getArgs()) === 1 && $leftType->isInteger()->yes() @@ -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 ) { @@ -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 ) { @@ -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) @@ -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 ) { @@ -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]) ) { @@ -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]) ) { @@ -1673,6 +1684,7 @@ private function specifyTypesForConstantStringBinaryExpression( $context->false() && $exprNode instanceof FuncCall && $exprNode->name instanceof Name + && !$exprNode->isFirstClassCallable() && in_array(strtolower((string) $exprNode->name), [ 'trim', 'ltrim', 'rtrim', 'chop', 'mb_trim', 'mb_ltrim', 'mb_rtrim', @@ -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) && isset($exprNode->getArgs()[0]) && $constantType->isString()->yes() @@ -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) @@ -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 ) { @@ -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()) { @@ -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() @@ -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]) ) { @@ -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', diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 135aad26f8..7522b5475f 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -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 diff --git a/tests/PHPStan/Analyser/data/bug-14550.php b/tests/PHPStan/Analyser/data/bug-14550.php new file mode 100644 index 0000000000..95b5ecb4ac --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-14550.php @@ -0,0 +1,130 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14550; + +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++) {} +}