Skip to content

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

Merged
VincentLanglet merged 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-njt1inu
Apr 7, 2026
Merged

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When an array is initialized as empty before a while loop and elements are added inside the loop body, PHPStan incorrectly kept treating the array as array{} (empty) throughout the loop. This caused false positives like in_array() with array{} will always evaluate to false.

The root cause was in TypeSpecifier::specifyTypesInCondition where count comparisons with IntegerRangeType values (e.g., count($codes) < $quantity where $quantity is int<1, 42>) were using the range directly as sizeType, causing overly aggressive array type narrowing in the falsey context.

Changes

  • src/Analyser/TypeSpecifier.php: Fixed sizeType computation for IntegerRangeType in count comparisons:
    • In falsey context (e.g., truthy scope of count($x) < $rangeVar): use max of the range to create a half-open range, so only arrays guaranteed to satisfy the condition for ALL possible values are removed
    • In truthy context: use min of the range, matching the existing behavior for constant integers
  • tests/PHPStan/Analyser/nsrt/bug-13705.php: New regression test reproducing the original issue
  • tests/PHPStan/Analyser/nsrt/bug11480.php: Updated test expectation to reflect correct narrowing
  • tests/PHPStan/Analyser/nsrt/list-count.php: Updated test expectations for IntegerRangeType count comparisons

Root cause

When processing count($codes) < $quantity where $quantity is int<1, 42>:

  1. The expression is transformed to NOT($quantity <= count($codes))
  2. $quantity <= count($codes) is processed in FALSE context
  3. sizeType was set to int<1, 42> (the full range)
  4. The specifyTypesForCountFuncCall fallback removed constant arrays whose size fell within [1, 42]
  5. This incorrectly removed arrays with count 1, even though count < $quantity is satisfiable when $quantity > 1

The fix computes sizeType differently for IntegerRangeType:

  • Falsey context: sizeType = int<max(range), ∞) — only remove arrays where the condition is guaranteed true for ALL possible values
  • Truthy context: sizeType = int<min(range), ∞) — keep arrays where the condition CAN be true

This caused the while loop's scope generalization to break: on iteration 1, after merging the loop body scope (where $codes grew) with the outer scope, the condition re-evaluation would narrow $codes back to array{}, making the scope equal to the previous iteration and prematurely terminating the generalization loop.

Test

Added tests/PHPStan/Analyser/nsrt/bug-13705.php with two test cases:

  1. whileLoop(): Array grows inside a while loop with count($codes) < $quantity condition — verifies the array type is correctly generalized to list<non-empty-string> inside the loop
  2. whileLoopOriginal(): The original issue's code with in_array() inside the while loop — verifies no false positive

Fixes phpstan/phpstan#13705

…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
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test for all the condition you added

@VincentLanglet VincentLanglet self-assigned this Apr 6, 2026
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>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

make tests also completed successfully — all 11736 tests passed. Both background tasks are done and the commit has already been pushed.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.php with three test cases (simple while loop, compound condition with $quantity >= 1 &&, and the original issue's class method)
  • Added testBug13705() to ImpossibleCheckTypeFunctionCallRuleTest expecting 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 $quantity is int<1, max> (unbounded max, e.g. after $quantity >= 1 narrowing), the falsey context fell through to the min-based branch, producing sizeType = int<1, max> which incorrectly narrowed arrays to array{}
  • Fixed by changing elseif ($leftType->getMin() !== null) to elseif ($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, $sizeType is left as null and no count narrowing occurs (correct behavior since we can't determine a definite threshold)
  • Updated bug-4700.php test expectations which were previously overly precise (narrowing to array{} when the range was unbounded)

phpstan-bot and others added 2 commits April 7, 2026 07:01
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-use the file in tests/PHPStan/Analyser/nsrt/bug-13705.php instead of duplicating it for the rule-test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not double check the added tests.

@VincentLanglet VincentLanglet merged commit 67e37ac into phpstan:2.1.x Apr 7, 2026
655 of 657 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-njt1inu branch April 7, 2026 08:21
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants