Skip to content
Draft
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
33 changes: 32 additions & 1 deletion src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
224 changes: 224 additions & 0 deletions tests/PHPStan/Analyser/nsrt/list-count2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?php

namespace ListCount2;

use function PHPStan\dumpType;
use function PHPStan\debugScope;
use function PHPStan\Testing\assertType;

class HelloWorld
{
/**
* @param non-empty-list<int> $listA
* @param list<int> $listB
*/
public function sayIdenticalLists($listA, array $listB): void
{
if (count($listA) === count($listB)) {
assertType('non-empty-list<int>', $listA);
assertType('non-empty-list<int>', $listB);
}
assertType('non-empty-list<int>', $listA);
assertType('list<int>', $listB);
}

/**
* @param non-empty-list<int> $listA
*/
public function sayIdenticalList($listA, array $arrB): void
{
if (count($listA) === count($arrB)) {
assertType('non-empty-list<int>', $listA);
assertType('non-empty-array', $arrB);
}
assertType('non-empty-list<int>', $listA);
assertType('array', $arrB);
}

/**
* @param non-empty-array<int> $arrA
*/
public function sayEqualArray($arrA, array $arrB): void
{
if (count($arrA) == count($arrB)) {
assertType('non-empty-array<int>', $arrA);
assertType('non-empty-array', $arrB);
}
assertType('non-empty-array<int>', $arrA);
assertType('array', $arrB);
}

/**
* @param non-empty-array<int> $arrA
* @param array<int> $arrB
*/
public function sayEqualIntArray($arrA, array $arrB): void
{
if (count($arrA) == count($arrB)) {
assertType('non-empty-array<int>', $arrA);
assertType('non-empty-array<int>', $arrB);
}
assertType('non-empty-array<int>', $arrA);
assertType('array<int>', $arrB);
}

/**
* @param non-empty-array<int> $arrA
* @param array<string> $arrB
*/
public function sayEqualStringArray($arrA, array $arrB): void
{
if (count($arrA) == count($arrB)) {
assertType('non-empty-array<int>', $arrA);
assertType('non-empty-array<string>', $arrB);
}
assertType('non-empty-array<int>', $arrA);
assertType('array<string>', $arrB);
}

/**
* @param array<int> $arrA
* @param array<string> $arrB
*/
public function sayUnknownSizeArray($arrA, array $arrB): void
{
if (count($arrA) == count($arrB)) {
assertType('array<int>', $arrA);
assertType('array<string>', $arrB);
}
assertType('array<int>', $arrA);
assertType('array<string>', $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<int> $listA
* @param list<int> $listB
*/
public function supportsNormalCount($listA, array $listB): void
{
if (count($listA, COUNT_NORMAL) === count($listB)) {
assertType('non-empty-list<int>', $listA);
assertType('non-empty-list<int>', $listB);
}
assertType('non-empty-list<int>', $listA);
assertType('list<int>', $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<int> $arrA
* @param array<int> $arrB
*/
public function skipRecursiveCount($arrA, array $arrB): void
{
if (count($arrA, COUNT_RECURSIVE) == count($arrB)) {
assertType('non-empty-array<int>', $arrA);
assertType('non-empty-array<int>', $arrB);
}
assertType('non-empty-array<int>', $arrA);
assertType('array<int>', $arrB);
}

}
Loading