diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6698a8a800..82e0e5d460 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2260,7 +2260,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $rightType = $scope->getType($rightExpr); - // (count($a) === $b) + // (count($a) === $expr) if ( !$context->null() && $unwrappedLeftExpr instanceof FuncCall @@ -2269,6 +2269,37 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty && in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true) && $rightType->isInteger()->yes() ) { + // count($a) === count($b) + if ( + $context->true() + && $unwrappedRightExpr instanceof FuncCall + && $unwrappedRightExpr->name instanceof Name + && in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true) + && count($unwrappedRightExpr->getArgs()) >= 1 + ) { + $argType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); + $sizeType = $scope->getType($leftExpr); + + $specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedRightExpr, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + + $leftArrayType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $rightArrayType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); + if ( + $leftArrayType->isArray()->yes() + && $rightArrayType->isArray()->yes() + && !$rightType->isConstantScalarValue()->yes() + && ($leftArrayType->isIterableAtLeastOnce()->yes() || $rightArrayType->isIterableAtLeastOnce()->yes()) + ) { + $arrayTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr); + return $arrayTypes->unionWith( + $this->create($unwrappedRightExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr), + ); + } + } + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); } diff --git a/tests/PHPStan/Analyser/nsrt/list-count2.php b/tests/PHPStan/Analyser/nsrt/list-count2.php new file mode 100644 index 0000000000..500705062f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/list-count2.php @@ -0,0 +1,224 @@ + $listA + * @param list $listB + */ + public function sayIdenticalLists($listA, array $listB): void + { + if (count($listA) === count($listB)) { + assertType('non-empty-list', $listA); + assertType('non-empty-list', $listB); + } + assertType('non-empty-list', $listA); + assertType('list', $listB); + } + + /** + * @param non-empty-list $listA + */ + public function sayIdenticalList($listA, array $arrB): void + { + if (count($listA) === count($arrB)) { + assertType('non-empty-list', $listA); + assertType('non-empty-array', $arrB); + } + assertType('non-empty-list', $listA); + assertType('array', $arrB); + } + + /** + * @param non-empty-array $arrA + */ + public function sayEqualArray($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('non-empty-array', $arrA); + assertType('non-empty-array', $arrB); + } + assertType('non-empty-array', $arrA); + assertType('array', $arrB); + } + + /** + * @param non-empty-array $arrA + * @param array $arrB + */ + public function sayEqualIntArray($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('non-empty-array', $arrA); + assertType('non-empty-array', $arrB); + } + assertType('non-empty-array', $arrA); + assertType('array', $arrB); + } + + /** + * @param non-empty-array $arrA + * @param array $arrB + */ + public function sayEqualStringArray($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('non-empty-array', $arrA); + assertType('non-empty-array', $arrB); + } + assertType('non-empty-array', $arrA); + assertType('array', $arrB); + } + + /** + * @param array $arrA + * @param array $arrB + */ + public function sayUnknownSizeArray($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('array', $arrA); + assertType('array', $arrB); + } + assertType('array', $arrA); + assertType('array', $arrB); + } + + /** + * @param array{int, int, int} $arrA + * @param list $arrB + */ + function sayEqualArrayShape($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('array{int, int, int}', $arrA); + assertType('array{mixed, mixed, mixed}', $arrB); + } + assertType('array{int, int, int}', $arrA); + assertType('list', $arrB); + } + + /** + * @param list $arrA + * @param array{int, int, int} $arrB + */ + function sayEqualArrayShapeReversed($arrA, array $arrB): void + { + if (count($arrA) == count($arrB)) { + assertType('array{mixed, mixed, mixed}', $arrA); + assertType('array{int, int, int}', $arrB); + } + assertType('list', $arrA); + assertType('array{int, int, int}', $arrB); + } + + /** + * @param array{int, int, int} $arrA + * @param list $arrB + */ + function sayEqualArrayShapeAfterNarrowedCount($arrA, array $arrB): void + { + if (count($arrB) < 2) { + return; + } + + if (count($arrA) == count($arrB)) { + assertType('array{int, int, int}', $arrA); + assertType('array{mixed, mixed, mixed}', $arrB); + } + assertType('array{int, int, int}', $arrA); + assertType('non-empty-list', $arrB); + } + + /** + * @param non-empty-array $arrB + */ + function dontNarrowEmpty(array $arrB): void + { + $arrA = []; + assertType('array{}', $arrA); + + if (count($arrA) == count($arrB)) { + assertType('*NEVER*', $arrA); + assertType('non-empty-array', $arrB); // could be '*NEVER*' + } + assertType('array{}', $arrA); + + if (count($arrB) == count($arrA)) { + assertType('*NEVER*', $arrA); + assertType('non-empty-array', $arrB); // could be '*NEVER*' + } + assertType('array{}', $arrA); + } + + /** + * @param non-empty-list $listA + * @param list $listB + */ + public function supportsNormalCount($listA, array $listB): void + { + if (count($listA, COUNT_NORMAL) === count($listB)) { + assertType('non-empty-list', $listA); + assertType('non-empty-list', $listB); + } + assertType('non-empty-list', $listA); + assertType('list', $listB); + } + + /** + * @param array{int, int, int} $arrA + * @param list $arrB + */ + function skipRecursiveLeftCount($arrA, array $arrB): void + { + if (count($arrB) < 2) { + return; + } + + if (count($arrA, COUNT_RECURSIVE) == count($arrB)) { + assertType('array{int, int, int}', $arrA); + assertType('non-empty-list', $arrB); + } + assertType('array{int, int, int}', $arrA); + assertType('non-empty-list', $arrB); + } + + /** + * @param array{int, int, int} $arrA + * @param list $arrB + */ + function skipRecursiveRightCount($arrA, array $arrB): void + { + if (count($arrB) < 2) { + return; + } + + if (count($arrA) == count($arrB, COUNT_RECURSIVE)) { + assertType('array{int, int, int}', $arrA); + assertType('non-empty-list', $arrB); + } + assertType('array{int, int, int}', $arrA); + assertType('non-empty-list', $arrB); + } + + /** + * @param non-empty-array $arrA + * @param array $arrB + */ + public function skipRecursiveCount($arrA, array $arrB): void + { + if (count($arrA, COUNT_RECURSIVE) == count($arrB)) { + assertType('non-empty-array', $arrA); + assertType('non-empty-array', $arrB); + } + assertType('non-empty-array', $arrA); + assertType('array', $arrB); + } + +}