diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 213ed039a1..6b5ec4c0ee 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2819,6 +2819,10 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); + if ($expr instanceof Variable) { + $this->refineDependentFuncCallTypes($exprString, $type, $nativeType, $expressionTypes, $nativeTypes); + } + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), @@ -2845,6 +2849,63 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, return $scope; } + /** + * @param array $expressionTypes + * @param array $nativeTypes + */ + private function refineDependentFuncCallTypes( + string $varExprString, + Type $varType, + Type $varNativeType, + array &$expressionTypes, + array &$nativeTypes, + ): void + { + foreach ($expressionTypes as $exprString => $holder) { + $holderExpr = $holder->getExpr(); + if (!$holderExpr instanceof FuncCall || !$holderExpr->name instanceof Name) { + continue; + } + if ($holderExpr->isFirstClassCallable() || count($holderExpr->getArgs()) < 1) { + continue; + } + if ($this->getNodeKey($holderExpr->getArgs()[0]->value) !== $varExprString) { + continue; + } + + $funcName = $holderExpr->name->toLowerString(); + $constraint = null; + $nativeConstraint = null; + if (in_array($funcName, ['count', 'sizeof'], true)) { + $constraint = $varType->getArraySize(); + $nativeConstraint = $varNativeType->getArraySize(); + } elseif (in_array($funcName, ['strlen', 'mb_strlen'], true)) { + if ($varType->isNonEmptyString()->yes()) { + $constraint = IntegerRangeType::fromInterval(1, null); + } + if ($varNativeType->isNonEmptyString()->yes()) { + $nativeConstraint = IntegerRangeType::fromInterval(1, null); + } + } + + if ($constraint !== null) { + $expressionTypes[$exprString] = new ExpressionTypeHolder( + $holderExpr, + TypeCombinator::intersect($holder->getType(), $constraint), + $holder->getCertainty(), + ); + } + + if ($nativeConstraint !== null && array_key_exists($exprString, $nativeTypes)) { + $nativeTypes[$exprString] = new ExpressionTypeHolder( + $holderExpr, + TypeCombinator::intersect($nativeTypes[$exprString]->getType(), $nativeConstraint), + $nativeTypes[$exprString]->getCertainty(), + ); + } + } + } + public function assignExpression(Expr $expr, Type $type, Type $nativeType): self { $scope = $this; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13750.php b/tests/PHPStan/Analyser/nsrt/bug-13750.php new file mode 100644 index 0000000000..04b7aebb00 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13750.php @@ -0,0 +1,84 @@ + $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 sizeofAfterCount(array $arr): void + { + if (sizeof($arr) > 2) { + assertType('int<3, max>', sizeof($arr)); + } + assertType('int<0, max>', sizeof($arr)); + if (sizeof($arr, COUNT_RECURSIVE) > 2) { + assertType('non-empty-array', $arr); + assertType('int<1, max>', sizeof($arr)); + } + } + + /** @param array $arr */ + public function countAfterNonEmptyNarrowing(array $arr): void + { + if (count($arr) > 2) { + assertType('int<3, max>', count($arr)); + } + assertType('int<0, max>', count($arr)); + if ($arr !== []) { + assertType('non-empty-array', $arr); + assertType('int<1, max>', count($arr)); + } + } + + /** @param array $arr */ + public function nestedCountPreservation(array $arr): void + { + if (count($arr) > 2) { + assertType('int<3, max>', count($arr)); + if (count($arr, COUNT_RECURSIVE) > 10) { + assertType('int<3, max>', count($arr)); + } + } + } + + public function strlenAfterNonEmptyNarrowing(string $str): void + { + if (strlen($str) > 5) { + assertType('int<6, max>', strlen($str)); + } + assertType('int<0, max>', strlen($str)); + if ($str !== '') { + assertType('non-empty-string', $str); + assertType('int<1, max>', strlen($str)); + } + } + + public function mbStrlenAfterNonEmptyNarrowing(string $str): void + { + if (mb_strlen($str) > 5) { + assertType('int<6, max>', mb_strlen($str)); + } + assertType('int<0, max>', mb_strlen($str)); + if ($str !== '') { + assertType('non-empty-string', $str); + assertType('int<1, max>', mb_strlen($str)); + } + } +}