From 83af88c851c7deb104a4b5df0b5c55a3f14f0459 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:00:09 +0000 Subject: [PATCH 1/3] Pre-compute count-specific conditional expressions to narrow list types when `count()` is stored in a variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When `$count = count($list)` is assigned, pre-compute conditional expressions for count values 1-8 so that `$count === N` narrows `$list` to the exact array shape (e.g. `array{T, T, T}` for N=3) - Previously, only inline `count($list) === 3` narrowed correctly; storing the count in a variable only gave `non-empty-list` - The fix extends AssignHandler to call specifyTypesInCondition with synthetic `count($expr) === N` comparisons for small N values, storing the results as ConditionalExpressionHolders - Works for count() and sizeof() with a single argument on list and constant array types - Analogous cases verified: sizeof() alias, explode() results, non-empty-list types, switch statements, PHPDoc list types - strlen() variable narrowing is a separate pre-existing issue with a different mechanism (no TypeSpecifyingExtension) — not addressed --- src/Analyser/ExprHandler/AssignHandler.php | 41 +++++++ .../Analyser/nsrt/bug-14464-analogous.php | 102 ++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14464.php | 72 +++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14464.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 51995ac6657..22877121b96 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -73,6 +73,7 @@ use function in_array; use function is_int; use function is_string; +use function strtolower; /** * @implements ExprHandler @@ -81,6 +82,8 @@ final class AssignHandler implements ExprHandler { + private const COUNT_CONDITIONAL_LIMIT = 8; + public function __construct( private TypeSpecifier $typeSpecifier, private PhpVersion $phpVersion, @@ -313,6 +316,44 @@ public function processAssignVar( $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); } + if ( + $assignedExpr instanceof FuncCall + && $assignedExpr->name instanceof Name + && in_array(strtolower($assignedExpr->name->toString()), ['count', 'sizeof'], true) + && count($assignedExpr->getArgs()) === 1 + && !$type instanceof ConstantIntegerType + ) { + $countArgType = $scope->getType($assignedExpr->getArgs()[0]->value); + if ($countArgType->isArray()->yes() && ($countArgType->isList()->yes() || $countArgType->isConstantArray()->yes())) { + for ($n = 1; $n <= self::COUNT_CONDITIONAL_LIMIT; $n++) { + $nType = new ConstantIntegerType($n); + $identicalExpr = new Expr\BinaryOp\Identical( + $assignedExpr, + new Node\Scalar\Int_($n), + ); + $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition( + $scope, + $identicalExpr, + TypeSpecifierContext::createTrue(), + ); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign( + $scope, + $var->name, + $conditionalExpressions, + $identicalSpecifiedTypes, + $nType, + ); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign( + $scope, + $var->name, + $conditionalExpressions, + $identicalSpecifiedTypes, + $nType, + ); + } + } + } + $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php new file mode 100644 index 00000000000..5585c410e43 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php @@ -0,0 +1,102 @@ + $items + */ +function testSizeof(array $items): void { + $count = sizeof($items); + if ($count === 3) { + assertType('array{int, int, int}', $items); + } +} + +/** + * Inline count still works + * @param list $items + */ +function testInlineCount(array $items): void { + if (count($items) === 3) { + assertType('array{int, int, int}', $items); + } +} + +/** + * explode() result + */ +function testExplode(string $input): void { + $parts = explode(',', $input); + $count = count($parts); + if ($count === 3) { + assertType('array{string, string, string}', $parts); + } elseif ($count === 1) { + assertType('array{string}', $parts); + } +} + +/** + * Variable count >= N (range comparison) + * @param list $items + */ +function testGreaterOrEqual(array $items): void { + $count = count($items); + if ($count >= 3) { + assertType('non-empty-list', $items); + } +} + +/** + * Count value > 8 (beyond pre-computed limit) + * @param list $items + */ +function testBeyondLimit(array $items): void { + $count = count($items); + if ($count === 10) { + assertType('non-empty-list', $items); + } +} + +/** + * Count with mode argument excluded from pre-computation + * @param list $items + */ +function testCountWithMode(array $items, int $mode): void { + $count = count($items, $mode); + if ($count === 3) { + assertType('non-empty-list', $items); + } +} + +/** + * Variable count on non-empty-list + * @param non-empty-list $items + */ +function testNonEmptyList(array $items): void { + $count = count($items); + if ($count === 2) { + assertType('array{string, string}', $items); + } +} + +/** + * Variable count with switch statement + * @param list $items + */ +function testSwitch(array $items): void { + $count = count($items); + switch ($count) { + case 1: + assertType('array{int}', $items); + break; + case 2: + assertType('array{int, int}', $items); + break; + case 3: + assertType('array{int, int, int}', $items); + break; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14464.php b/tests/PHPStan/Analyser/nsrt/bug-14464.php new file mode 100644 index 00000000000..65bef905817 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14464.php @@ -0,0 +1,72 @@ +', $colParts); + $numParts = count($colParts); + + if ($numParts == 3) { + assertType('array{non-empty-string, non-empty-string, non-empty-string}', $colParts); + $this->columnName($colParts[0]); + $this->columnName($colParts[1]); + $this->columnName($colParts[2]); + } elseif ($numParts == 2) { + assertType('array{non-empty-string, non-empty-string}', $colParts); + $this->columnName($colParts[0]); + $this->columnName($colParts[1]); + } elseif ($numParts == 1) { + assertType('array{non-empty-string}', $colParts); + $this->columnName($colParts[0]); + } else { + throw new \LogicException('invalid'); + } + } + + /** Variable count with === (strict comparison) */ + protected function strictComparison(string $input): void + { + $parts = preg_split('/,/', $input, -1, \PREG_SPLIT_NO_EMPTY); + if ($parts === false) { + throw new \RuntimeException('preg error'); + } + $count = count($parts); + + if ($count === 3) { + assertType('array{non-empty-string, non-empty-string, non-empty-string}', $parts); + } elseif ($count === 1) { + assertType('array{non-empty-string}', $parts); + } + } + + /** + * Variable count on a PHPDoc list type + * @param list $items + */ + protected function phpdocList(array $items): void + { + $count = count($items); + if ($count === 3) { + assertType('array{int, int, int}', $items); + } elseif ($count === 5) { + assertType('array{int, int, int, int, int}', $items); + } else { + assertType('list', $items); + } + } + + public function columnName(string $columnName): string + { + return 'abc'; + } +} From 5b65941b806d9d3c8a445c16bde3c2545d9bb555 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 16 Apr 2026 05:56:04 +0000 Subject: [PATCH 2/3] Generalize integer conditional pre-computation to all FuncCall expressions Instead of hardcoding count()/sizeof() in AssignHandler, delegate to TypeSpecifier which already knows which functions benefit from value-specific narrowing. This removes the special case and makes the mechanism work for any integer-returning FuncCall (e.g. strlen). Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/AssignHandler.php | 60 +++++++++---------- .../Analyser/nsrt/bug-14464-analogous.php | 16 ++++- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 22877121b96..e050f0c7ae4 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -73,7 +73,6 @@ use function in_array; use function is_int; use function is_string; -use function strtolower; /** * @implements ExprHandler @@ -82,7 +81,7 @@ final class AssignHandler implements ExprHandler { - private const COUNT_CONDITIONAL_LIMIT = 8; + private const INTEGER_CONDITIONAL_LIMIT = 8; public function __construct( private TypeSpecifier $typeSpecifier, @@ -318,39 +317,34 @@ public function processAssignVar( if ( $assignedExpr instanceof FuncCall - && $assignedExpr->name instanceof Name - && in_array(strtolower($assignedExpr->name->toString()), ['count', 'sizeof'], true) - && count($assignedExpr->getArgs()) === 1 + && $type->isInteger()->yes() && !$type instanceof ConstantIntegerType ) { - $countArgType = $scope->getType($assignedExpr->getArgs()[0]->value); - if ($countArgType->isArray()->yes() && ($countArgType->isList()->yes() || $countArgType->isConstantArray()->yes())) { - for ($n = 1; $n <= self::COUNT_CONDITIONAL_LIMIT; $n++) { - $nType = new ConstantIntegerType($n); - $identicalExpr = new Expr\BinaryOp\Identical( - $assignedExpr, - new Node\Scalar\Int_($n), - ); - $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition( - $scope, - $identicalExpr, - TypeSpecifierContext::createTrue(), - ); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign( - $scope, - $var->name, - $conditionalExpressions, - $identicalSpecifiedTypes, - $nType, - ); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign( - $scope, - $var->name, - $conditionalExpressions, - $identicalSpecifiedTypes, - $nType, - ); - } + for ($n = 1; $n <= self::INTEGER_CONDITIONAL_LIMIT; $n++) { + $nType = new ConstantIntegerType($n); + $identicalExpr = new Expr\BinaryOp\Identical( + $assignedExpr, + new Node\Scalar\Int_($n), + ); + $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition( + $scope, + $identicalExpr, + TypeSpecifierContext::createTrue(), + ); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign( + $scope, + $var->name, + $conditionalExpressions, + $identicalSpecifiedTypes, + $nType, + ); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign( + $scope, + $var->name, + $conditionalExpressions, + $identicalSpecifiedTypes, + $nType, + ); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php index 5585c410e43..0fb3b791b36 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php @@ -61,13 +61,25 @@ function testBeyondLimit(array $items): void { } /** - * Count with mode argument excluded from pre-computation + * Count with mode argument - safe for list since int values are not countable * @param list $items */ function testCountWithMode(array $items, int $mode): void { $count = count($items, $mode); if ($count === 3) { - assertType('non-empty-list', $items); + assertType('array{int, int, int}', $items); + } +} + +/** + * Variable strlen - generalized integer pre-computation also works for strlen + */ +function testStrlen(string $s): void { + $len = strlen($s); + if ($len === 3) { + assertType('non-falsy-string', $s); + } elseif ($len === 1) { + assertType('non-empty-string', $s); } } From 74353c3cfe38e65120b050482a4be66b4265f259 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 16 Apr 2026 06:30:36 +0000 Subject: [PATCH 3/3] Remember FuncCall expression on variable assignment instead of pre-computing conditional expressions Instead of pre-computing conditional expressions for integer values 1-8 at assignment time, remember the original FuncCall expression that was assigned to a variable. When TypeSpecifier later resolves a comparison like `$var === N`, it looks up the remembered expression and resolves `funcCall(...) === N` dynamically, producing the same precise type narrowing without an arbitrary limit. This addresses the review feedback asking to remember the expression a variable was assigned from. The approach: - ExpressionTypeHolder gains an optional `assignedFromExpr` field that survives type narrowing, branch merges, and scope transitions naturally (since it lives in the expressionTypes array which is already threaded through all scope operations) - AssignHandler sets `assignedFromExpr` when a variable is assigned from a FuncCall - TypeSpecifier::resolveIdentical() checks for remembered expressions and resolves the comparison with the original FuncCall substituted in - The remembered expression is automatically cleared on variable reassignment (invalidateExpression removes the old ExpressionTypeHolder) Benefits over the pre-computation approach: - No arbitrary limit (count === 10 now produces array{T, T, T, T, T, T, T, T, T, T}) - More memory-efficient (one expression reference vs N conditional expressions) - Works for any FuncCall, resolved dynamically by TypeSpecifier Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/AssignHandler.php | 43 ++++--------------- src/Analyser/ExpressionTypeHolder.php | 29 +++++++++++-- src/Analyser/MutatingScope.php | 22 +++++++++- src/Analyser/TypeSpecifier.php | 21 +++++++++ .../Analyser/nsrt/bug-14464-analogous.php | 6 +-- 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index e050f0c7ae4..8a237ca2acc 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -81,8 +81,6 @@ final class AssignHandler implements ExprHandler { - private const INTEGER_CONDITIONAL_LIMIT = 8; - public function __construct( private TypeSpecifier $typeSpecifier, private PhpVersion $phpVersion, @@ -315,45 +313,20 @@ public function processAssignVar( $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); } - if ( - $assignedExpr instanceof FuncCall - && $type->isInteger()->yes() - && !$type instanceof ConstantIntegerType - ) { - for ($n = 1; $n <= self::INTEGER_CONDITIONAL_LIMIT; $n++) { - $nType = new ConstantIntegerType($n); - $identicalExpr = new Expr\BinaryOp\Identical( - $assignedExpr, - new Node\Scalar\Int_($n), - ); - $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition( - $scope, - $identicalExpr, - TypeSpecifierContext::createTrue(), - ); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign( - $scope, - $var->name, - $conditionalExpressions, - $identicalSpecifiedTypes, - $nType, - ); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign( - $scope, - $var->name, - $conditionalExpressions, - $identicalSpecifiedTypes, - $nType, - ); - } - } - $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); } + if ($assignedExpr instanceof FuncCall) { + $varExprString = '$' . $var->name; + $existingHolder = $scope->expressionTypes[$varExprString] ?? null; + if ($existingHolder !== null) { + $scope->expressionTypes[$varExprString] = $existingHolder->withAssignedFromExpr($assignedExpr); + } + } + if ($assignedExpr instanceof Expr\Array_) { $scope = $this->processArrayByRefItems($scope, $var->name, $assignedExpr, new Variable($var->name)); } diff --git a/src/Analyser/ExpressionTypeHolder.php b/src/Analyser/ExpressionTypeHolder.php index 7477dbf3dcd..3f02127669e 100644 --- a/src/Analyser/ExpressionTypeHolder.php +++ b/src/Analyser/ExpressionTypeHolder.php @@ -14,6 +14,7 @@ public function __construct( private readonly Expr $expr, private readonly Type $type, private readonly TrinaryLogic $certainty, + private readonly ?Expr $assignedFromExpr = null, ) { } @@ -52,22 +53,34 @@ public function equals(self $other): bool public function and(self $other): self { + $assignedFromExpr = $this->assignedFromExpr === $other->assignedFromExpr ? $this->assignedFromExpr : null; + if ($this->type === $other->type || $this->type->equals($other->type)) { if ($this->certainty->and($other->certainty)->yes()) { - return $this; + if ($assignedFromExpr === $this->assignedFromExpr) { + return $this; + } + return $this->withAssignedFromExpr($assignedFromExpr); } if ($this->certainty->maybe()) { - return $this; + if ($assignedFromExpr === $this->assignedFromExpr) { + return $this; + } + return $this->withAssignedFromExpr($assignedFromExpr); } - return $other; + if ($assignedFromExpr === $other->assignedFromExpr) { + return $other; + } + return $other->withAssignedFromExpr($assignedFromExpr); } return new self( $this->expr, TypeCombinator::union($this->type, $other->type), $this->certainty->and($other->certainty), + $assignedFromExpr, ); } @@ -86,4 +99,14 @@ public function getCertainty(): TrinaryLogic return $this->certainty; } + public function getAssignedFromExpr(): ?Expr + { + return $this->assignedFromExpr; + } + + public function withAssignedFromExpr(?Expr $assignedFromExpr): self + { + return new self($this->expr, $this->type, $this->certainty, $assignedFromExpr); + } + } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27b..e8cb6b70bb9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -685,6 +685,16 @@ public function getVariableType(string $variableName): Type return $this->expressionTypes[$varExprString]->getType(); } + public function getVariableAssignedFromExpr(string $variableName): ?Expr + { + $varExprString = '$' . $variableName; + if (!array_key_exists($varExprString, $this->expressionTypes)) { + return null; + } + + return $this->expressionTypes[$varExprString]->getAssignedFromExpr(); + } + /** * @api * @return list @@ -2765,7 +2775,8 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $exprString = $this->getNodeKey($expr); $expressionTypes = $scope->expressionTypes; - $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty); + $existingAssignedFromExpr = ($expressionTypes[$exprString] ?? null)?->getAssignedFromExpr(); + $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty, $existingAssignedFromExpr); $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); @@ -3272,6 +3283,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $scope->expressionTypes[$conditionalExprString]->getExpr(), TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type), TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty), + $scope->expressionTypes[$conditionalExprString]->getAssignedFromExpr(), ); } else { $scope->expressionTypes[$conditionalExprString] = $expressions[0]->getTypeHolder(); @@ -3887,10 +3899,14 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope continue; } + $assignedFromExpr = $variableTypeHolder->getAssignedFromExpr() === $expressionTypes[$variableExprString]->getAssignedFromExpr() + ? $variableTypeHolder->getAssignedFromExpr() + : null; $expressionTypes[$variableExprString] = new ExpressionTypeHolder( $variableTypeHolder->getExpr(), $variableTypeHolder->getType(), $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()), + $assignedFromExpr, ); } $nativeTypes = $this->nativeExpressionTypes; @@ -3991,10 +4007,14 @@ private function generalizeVariableTypeHolders( ) { $generalizedExpressions[$variableExprString] = $variableTypeHolder->getExpr(); } + $assignedFromExpr = $variableTypeHolder->getAssignedFromExpr() === $otherVariableTypeHolders[$variableExprString]->getAssignedFromExpr() + ? $variableTypeHolder->getAssignedFromExpr() + : null; $newVariableTypeHolders[$variableExprString] = new ExpressionTypeHolder( $variableTypeHolder->getExpr(), $generalizedType, $variableTypeHolder->getCertainty(), + $assignedFromExpr, ); } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3d4475d5938..c818033833f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2683,6 +2683,27 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty ); } + // When a variable was assigned from a FuncCall, also resolve with the original expression + if ($scope instanceof MutatingScope && !$leftExpr instanceof FuncCall && !$rightExpr instanceof FuncCall) { + $rememberedExpr = null; + if ($leftExpr instanceof Expr\Variable && is_string($leftExpr->name)) { + $rememberedExpr = $scope->getVariableAssignedFromExpr($leftExpr->name); + } elseif ($rightExpr instanceof Expr\Variable && is_string($rightExpr->name)) { + $rememberedExpr = $scope->getVariableAssignedFromExpr($rightExpr->name); + } + + if ($rememberedExpr instanceof FuncCall) { + $substitutedExpr = $rememberedExpr; + $otherExpr = $leftExpr instanceof Expr\Variable && is_string($leftExpr->name) ? $rightExpr : $leftExpr; + $funcCallSpecifiedTypes = $this->resolveNormalizedIdentical( + new Expr\BinaryOp\Identical($substitutedExpr, $otherExpr), + $scope, + $context, + ); + $specifiedTypes = $specifiedTypes->unionWith($funcCallSpecifiedTypes); + } + } + return $specifiedTypes; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php index 0fb3b791b36..836b58446d1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14464-analogous.php @@ -50,13 +50,13 @@ function testGreaterOrEqual(array $items): void { } /** - * Count value > 8 (beyond pre-computed limit) + * Count value > 8 (no longer limited by pre-computation) * @param list $items */ function testBeyondLimit(array $items): void { $count = count($items); if ($count === 10) { - assertType('non-empty-list', $items); + assertType('array{int, int, int, int, int, int, int, int, int, int}', $items); } } @@ -72,7 +72,7 @@ function testCountWithMode(array $items, int $mode): void { } /** - * Variable strlen - generalized integer pre-computation also works for strlen + * Variable strlen - remembered expression also works for strlen */ function testStrlen(string $s): void { $len = strlen($s);