Skip to content

Add range() throw type extension - #6539

Open
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:range/throw-type
Open

zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:range/throw-type

Conversation

@zonuexe

@zonuexe zonuexe commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

PhpStorm stubs declare @throws \ValueError on range(), so every call is an explicit throw point and PHPStan does not report a catch around a valid call as dead:

try {
	$a = range(1, 10);
} catch (\ValueError $e) { // not reported before this PR
}

On PHP 8.0+, range() throws ValueError for:

  • a step of 0, or one wider than the range, which covers an infinite step;
  • a negative step on an increasing range, a NAN step and a step of PHP_INT_MIN (8.3+);
  • an infinite boundary, and a NAN one since 8.3;
  • a range with more items than an array can hold.

PHP 7 reported these with a warning and a false return value.

The extension looks at native types. It returns VoidType on PHP 7, and on PHP 8 when it can prove the call succeeds:

  • every argument is a constant number or string, and the step a number;
  • RangeFunctionArgumentsHelper finds that no analysed PHP version rejects the arguments. It checks the PHP 8.3 rules and the older ones separately. For a range of at most ARRAY_COUNT_LIMIT items it also uses the verdict of calling range(), but only when PHPStan itself runs on PHP 8.3 or newer;
  • the range has fewer items than RANGE_SIZE_LIMIT, the HT_MAX_SIZE of a 32-bit PHP. A range whose number of items does not fit into a float, such as range(-1.0e308, 1.0e308), keeps the throw point.

Otherwise it keeps the declared ValueError. Two cases stay undecided on purpose:

  • A string boundary before PHP 8.3. range('1', 'a', 30) is a character range on 8.3, while 8.2 turns '1' into a number and 'a' into 0, and throws because 30 exceeds the range 1..0.
  • Integers beyond 2 ** 53, which PHP compares exactly and a float cannot. range(0, 2 ** 60 - 1, 2 ** 60) throws on 8.2.

Since the extension reads $scope->getPhpVersion(), I split the test data by PHP_VERSION_ID. As in the sibling tests, a second method expects every catch to be dead on PHP 7, which has no ValueError, and the test expects range('a', 'z') to be decided for PHP 8.3 only when it runs on PHP 8.3 or newer. On PHP 7 the self-analysis reports the catch in RangeFunctionArgumentsHelper::callRange() as dead, so build/baseline-pre-8.0.neon ignores it. RangeFunctionArgumentsHelperTest covers the cases that depend on the PHP running PHPStan. The tests fail without the extension.

#6492 changes the return type of range() and uses the same helper. I cherry-picked the helper commits into it, so the two PRs merge in either order.

PHP 8.0-8.2 ignore the sign of the step and, for numeric boundaries,
reject one that is 0 or wider than the range; PHP 7 does the same with a
warning and false instead of a ValueError. PHP 8.3 checks a step of 0, a
NAN step and a step of PHP_INT_MIN before the boundaries, rejects a
negative step on an increasing range, and builds a character range from
two single bytes, in which a float step turns every character but a digit
into 0.

RangeFunctionArgumentsHelper::rejects() answers for the PHP versions of
the scope, with PhpVersions::hasStricterRangeFunction() telling them
apart. It uses the verdict of calling range() only when PHPStan itself
runs on PHP 8.3 or newer, and leaves undecided what it cannot tell
without running an older PHP: string boundaries before PHP 8.3, and
numbers beyond 2 ** 53, which PHP compares exactly as integers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@zonuexe
zonuexe force-pushed the range/throw-type branch 2 times, most recently from eff5b7f to 8c793b0 Compare September 22, 2026 21:09
PhpStorm stubs declare `@throws \ValueError` on range(), so every call
became an explicit throw point and a catch around a valid call was never
reported as dead.

The extension returns void on PHP 7, which does not throw, and on PHP 8
when the native argument types are constant numbers or strings,
RangeFunctionArgumentsHelper finds that no analysed PHP version rejects
them, and the range has fewer items than RANGE_SIZE_LIMIT, the
HT_MAX_SIZE of a 32-bit PHP. A range whose number of items does not fit
into a float keeps the throw point as well.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@zonuexe
zonuexe marked this pull request as ready for review September 22, 2026 21:15
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

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.

2 participants