diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27..39e8c4bfaf 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3279,6 +3279,51 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } + // Update stored FuncCall expression types whose arguments were narrowed. + // The stored type may be stale (from scope merging) while the argument + // types have been freshly narrowed. Intersect with the dynamically + // computed type so both sources of narrowing are preserved. + $funcCallsToUpdate = []; + foreach ($scope->expressionTypes as $exprString => $exprTypeHolder) { + if (array_key_exists($exprString, $specifiedExpressions) || array_key_exists($exprString, $conditions)) { + continue; + } + $expr = $exprTypeHolder->getExpr(); + if (!$expr instanceof FuncCall) { + continue; + } + foreach ($expr->getArgs() as $arg) { + $argKey = $this->getNodeKey($arg->value); + if (!array_key_exists($argKey, $specifiedExpressions)) { + continue; + } + $oldArgType = array_key_exists($argKey, $this->expressionTypes) + ? $this->expressionTypes[$argKey]->getType() + : null; + $newArgType = array_key_exists($argKey, $scope->expressionTypes) + ? $scope->expressionTypes[$argKey]->getType() + : null; + if ($oldArgType !== null && $newArgType !== null && $oldArgType->equals($newArgType)) { + continue; + } + $funcCallsToUpdate[$exprString] = $exprTypeHolder; + break; + } + } + + foreach ($funcCallsToUpdate as $exprString => $exprTypeHolder) { + $storedType = $exprTypeHolder->getType(); + unset($scope->expressionTypes[$exprString]); + unset($scope->nativeExpressionTypes[$exprString]); + unset($scope->resolvedTypes[$exprString]); + $dynamicType = $scope->getType($exprTypeHolder->getExpr()); + $scope->expressionTypes[$exprString] = new ExpressionTypeHolder( + $exprTypeHolder->getExpr(), + TypeCombinator::intersect($storedType, $dynamicType), + $exprTypeHolder->getCertainty(), + ); + } + return $scope->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), diff --git a/tests/PHPStan/Analyser/nsrt/bug-13750.php b/tests/PHPStan/Analyser/nsrt/bug-13750.php new file mode 100644 index 0000000000..30dc1a96cd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13750.php @@ -0,0 +1,52 @@ + $arr */ + public function forgetCount(array $arr): void + { + if (count($arr) > 2) { + assertType('non-empty-array', $arr); + assertType('int<3, max>', count($arr)); + } + assertType('array', $arr); + assertType('int<0, max>', count($arr)); + if (count($arr, COUNT_RECURSIVE) > 2) { + assertType('non-empty-array', $arr); + assertType('int<1, max>', count($arr)); + } + } + + /** @param array $arr */ + public function forgetCountDifferentNarrowing(array $arr): void + { + if (count($arr) > 2) { + } + if ($arr !== []) { + assertType('int<1, max>', count($arr)); + } + } + + /** @param array $arr */ + public function sizeofAfterSizeof(array $arr): void + { + if (sizeof($arr) > 2) { + } + if ($arr !== []) { + assertType('int<1, max>', sizeof($arr)); + } + } + + public function strlenAfterStrlen(string $str): void + { + if (strlen($str) > 5) { + } + if ($str !== '') { + assertType('int<1, max>', strlen($str)); + } + } +}