Fix phpstan/phpstan#13705: Adding elements to empty array in while loop does not result in array being recognized as potentially non-empty for in_array#5419
Conversation
…sively - Fixed TypeSpecifier to properly compute sizeType for count() comparisons when the compared value is an IntegerRangeType (e.g. int<1, 42>) - In falsey context, use max of range to avoid incorrectly removing arrays whose count falls within the range but can still satisfy the condition - In truthy context, use min of range for correct minimum count constraint - Updated test expectations in bug11480.php and list-count.php to reflect the more correct narrowing behavior - New regression test in tests/PHPStan/Analyser/nsrt/bug-13705.php
VincentLanglet
left a comment
There was a problem hiding this comment.
Add test for all the condition you added
Cover all branches in the IntegerRangeType handling in TypeSpecifier: - Falsey context with orEqual (count < bounded range) - Falsey context without orEqual (count <= bounded range) - Truthy context with orEqual (range <= count) - Truthy context without orEqual (range < count) - Fallback branch with unbounded min range (int<min, N>) - Unbounded max range (int<N, max>) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
VincentLanglet
left a comment
There was a problem hiding this comment.
Ensure the original issue
class HelloWorld
{
private const MIN_LENGTH = 8;
/**
* Generate given amount of plain recovery codes with the given length
*
* @return list<non-empty-string>
*/
public function generatePlainRecoveryCodes(int $length = 8, int $quantity = 8): array
{
if ($length < self::MIN_LENGTH) {
throw new \InvalidArgumentException(
$length . ' is not allowed as length for recovery codes. Must be at least ' . self::MIN_LENGTH,
1613666803
);
}
$codes = [];
while ($quantity >= 1 && count($codes) < $quantity) {
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= 'x';
}
// Prevent duplicate codes which is however very unlikely to happen
if (!in_array($code, $codes, true)) {
$codes[] = $code;
}
}
return $codes;
}
}
does not produce
Call to function in_array() with arguments literal-string&non-falsy-string, array{} and true will always evaluate to false.
anymore
Adds the HelloWorld class from phpstan/phpstan#13705 to ensure the exact code from the bug report no longer produces a false positive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Both background tasks confirmed complete. Everything was already committed and pushed — nothing left to do. |
| @@ -0,0 +1,158 @@ | |||
| <?php declare(strict_types = 1); | |||
|
|
|||
| namespace Bug13705; | |||
There was a problem hiding this comment.
the newly added bug-13705.php should also be checked against a rule test which verifies we will no longer get
"Call to function in_array() with arguments literal-string&non-falsy-string, array{} and true will always evaluate to false." and similar errors
There was a problem hiding this comment.
Pushed both commits. Here's a summary of the changes:
Commit 1: Add rule test for bug-13705
- Added
tests/PHPStan/Rules/Comparison/data/bug-13705.phpwith three test cases (simple while loop, compound condition with$quantity >= 1 &&, and the original issue's class method) - Added
testBug13705()toImpossibleCheckTypeFunctionCallRuleTestexpecting no errors
Commit 2: Fix IntegerRangeType count narrowing for falsey context with unbounded max
- The rule test exposed that the original fix was incomplete: when
$quantityisint<1, max>(unbounded max, e.g. after$quantity >= 1narrowing), the falsey context fell through to the min-based branch, producingsizeType = int<1, max>which incorrectly narrowed arrays toarray{} - Fixed by changing
elseif ($leftType->getMin() !== null)toelseif ($context->truthy() && $leftType->getMin() !== null)— the min-based branch should only apply in truthy context - When falsey with unbounded max and truthy with unbounded min,
$sizeTypeis left asnulland no count narrowing occurs (correct behavior since we can't determine a definite threshold) - Updated
bug-4700.phptest expectations which were previously overly precise (narrowing toarray{}when the range was unbounded)
Adds a rule test for ImpossibleCheckTypeFunctionCallRule that verifies in_array() inside while loops with count comparisons no longer produces false "will always evaluate to false" errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d max When the range has no upper bound (e.g. int<1, max>), the falsey context should not narrow using the min value, as this incorrectly eliminates arrays that could satisfy the condition for some (but not all) values in the range. Only set sizeType when the context is truthy and min is available, or when falsey and max is available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| @@ -0,0 +1,61 @@ | |||
| <?php declare(strict_types = 1); | |||
There was a problem hiding this comment.
re-use the file in tests/PHPStan/Analyser/nsrt/bug-13705.php instead of duplicating it for the rule-test
There was a problem hiding this comment.
The make tests background task also completed successfully. Everything is done — the review feedback has been addressed, tests and static analysis pass, and the commit is pushed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
staabm
left a comment
There was a problem hiding this comment.
I did not double check the added tests.
…loop does not result in array being recognized as potentially non-empty for `in_array` (phpstan#5419) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
When an array is initialized as empty before a
whileloop and elements are added inside the loop body, PHPStan incorrectly kept treating the array asarray{}(empty) throughout the loop. This caused false positives likein_array() with array{} will always evaluate to false.The root cause was in
TypeSpecifier::specifyTypesInConditionwhere count comparisons withIntegerRangeTypevalues (e.g.,count($codes) < $quantitywhere$quantityisint<1, 42>) were using the range directly assizeType, causing overly aggressive array type narrowing in the falsey context.Changes
src/Analyser/TypeSpecifier.php: Fixed sizeType computation forIntegerRangeTypein count comparisons:count($x) < $rangeVar): usemaxof the range to create a half-open range, so only arrays guaranteed to satisfy the condition for ALL possible values are removedminof the range, matching the existing behavior for constant integerstests/PHPStan/Analyser/nsrt/bug-13705.php: New regression test reproducing the original issuetests/PHPStan/Analyser/nsrt/bug11480.php: Updated test expectation to reflect correct narrowingtests/PHPStan/Analyser/nsrt/list-count.php: Updated test expectations for IntegerRangeType count comparisonsRoot cause
When processing
count($codes) < $quantitywhere$quantityisint<1, 42>:NOT($quantity <= count($codes))$quantity <= count($codes)is processed in FALSE contextsizeTypewas set toint<1, 42>(the full range)specifyTypesForCountFuncCallfallback removed constant arrays whose size fell within[1, 42]count < $quantityis satisfiable when$quantity > 1The fix computes
sizeTypedifferently forIntegerRangeType:sizeType = int<max(range), ∞)— only remove arrays where the condition is guaranteed true for ALL possible valuessizeType = int<min(range), ∞)— keep arrays where the condition CAN be trueThis caused the while loop's scope generalization to break: on iteration 1, after merging the loop body scope (where
$codesgrew) with the outer scope, the condition re-evaluation would narrow$codesback toarray{}, making the scope equal to the previous iteration and prematurely terminating the generalization loop.Test
Added
tests/PHPStan/Analyser/nsrt/bug-13705.phpwith two test cases:whileLoop(): Array grows inside a while loop withcount($codes) < $quantitycondition — verifies the array type is correctly generalized tolist<non-empty-string>inside the loopwhileLoopOriginal(): The original issue's code within_array()inside the while loop — verifies no false positiveFixes phpstan/phpstan#13705