Skip to content

Return int|float from AccessoryDecimalIntegerStringType::toNumber() to account for overflow#5815

Open
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-lmqyt67
Open

Return int|float from AccessoryDecimalIntegerStringType::toNumber() to account for overflow#5815
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-lmqyt67

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Casting a decimal-int-string to a number with the unary + operator (or via arithmetic) was inferred as int. Large decimal integer strings overflow PHP_INT_MAX/PHP_INT_MIN and become float at runtime, so the result is actually int|float. The wrong int inference also produced false positives like "Call to function is_int() with int will always evaluate to true."

This change makes the numeric coercion of a decimal-int-string produce int|float.

Changes

  • src/Type/Accessory/AccessoryDecimalIntegerStringType.php: toNumber() now returns int|float for the non-inverse case too (it previously returned just int). Both the inverse and non-inverse cases now return the same int|float union.
  • tests/PHPStan/Analyser/nsrt/decimal-int-string.php: updated the now-corrected assertion — $s + $s for a decimal-int-string is float|int (matching the non-decimal-int-string sibling, which already asserted float|int).
  • tests/PHPStan/Analyser/nsrt/bug-14786.php: new regression test.

Root cause

The decimal-int-string accessory type was designed primarily around array-key casting, where it models "string that becomes an integer key". Its toNumber() reused that integer assumption and returned a pure IntegerType. But numeric coercion (+$s, $s + 1, etc.) of a decimal integer string overflows to float once the value exceeds the platform int range, so the coerced type must be int|float.

The same numeric-overflow handling already existed in the parallel constructs:

  • AccessoryNumericStringType::toNumber() returns int|float.
  • The inverse non-decimal-int-string branch of AccessoryDecimalIntegerStringType::toNumber() already returned int|float.
  • ConstantStringType::toNumber() folds overflow to ConstantFloatType via +$value.

Only the non-inverse decimal-int-string branch was inconsistent. I also probed toArrayKey(), which has the same overflow edge case at runtime, but left it unchanged: that method encodes the type's documented contract (a decimal-int-string is treated as an integer array key) and changing it would be a separate, much larger semantic change affecting the type's purpose.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14786.php asserts +$s is float|int for decimal-int-string, and also covers the already-correct siblings (non-decimal-int-string, numeric-string), the (int) $s cast (stays int, no overflow to float), and arithmetic ($s + 1, $s * 2). The test fails before the fix (actual int) and passes after.
  • Updated decimal-int-string.php assertion verified to be the corrected (not buggy) behavior.

Fixes phpstan/phpstan#14786

…)` to account for overflow

- A non-inverse `decimal-int-string` previously coerced to a pure `int` via `toNumber()`, but decimal integer strings larger than PHP_INT_MAX (or smaller than PHP_INT_MIN) overflow to `float` when cast to a number (e.g. `+"9999999999999999999"`).
- `toNumber()` now returns `int|float` for both the inverse and non-inverse cases, matching `AccessoryNumericStringType::toNumber()` and the constant-string path (`ConstantStringType::toNumber()` already folds overflow to float via `+$value`).
- This fixes `+$s` / arithmetic on `decimal-int-string` being inferred as `int`, which caused false "is_int() will always evaluate to true" errors.
- Updated the existing `decimal-int-string` inference test (`$s + $s` is now `float|int`, consistent with the `non-decimal-int-string` sibling already asserting `float|int`).
- Probed siblings: `AccessoryNumericStringType::toNumber()` and the inverse `decimal-int-string` already returned `int|float`; `toArrayKey()` is left as-is since the type's contract is explicitly about array-key casting.
@staabm staabm requested a review from ondrejmirtes June 6, 2026 14:40
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.

add a test showing this pr no longer emits

Call to function is_int() with int will always evaluate to true.

on https://phpstan.org/r/b711705f-d5b4-423c-99be-4a60c7fa92a7

…-string

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I addressed staabm's review by adding a rule-level regression test demonstrating the is_int() false positive is gone.

What I added

tests/PHPStan/Rules/Comparison/data/bug-14786.php — a reproducer modeled on the nikic/PHP-Parser parseNumber() snippet from the issue: a decimal-int-string is coerced with +$str and then checked with is_int($num).

ImpossibleCheckTypeFunctionCallRuleTest::testBug14786() — analyses that file with treatPhpDocTypesAsCertain = true and expects no errors.

Verification

  • With the fix in place: test passes ✅
  • After temporarily reverting toNumber() back to returning int: the test fails with exactly the reported false positive — Call to function is_int() with int will always evaluate to true. (line 12) — confirming it guards the right behavior.
  • Full ImpossibleCheckTypeFunctionCallRuleTest: 99/99 passing.
  • make phpstan: no errors.

I committed this separately from the existing fix (commit 43bfe3aa0) and pushed to the PR branch. This complements the type-inference assertions already in nsrt/bug-14786.php by covering the actual rule that emitted the false positive in the issue.

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.

casting a decimal-int-string with + into a number, can lead to floats

2 participants