Skip to content

Treat decimal-int-string as a numeric string in looseCompare() instead of assuming inequality with non-decimal strings#5824

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-1cc49c6
Open

Treat decimal-int-string as a numeric string in looseCompare() instead of assuming inequality with non-decimal strings#5824
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-1cc49c6

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan reported Loose comparison using == between decimal-int-string and non-decimal-int-string will always evaluate to false (and inferred the result type false) for comparisons that are actually true at runtime. Because PHP compares numeric strings numerically, '2' == '02' and '2' == '2.0' are both true. The fix teaches AccessoryDecimalIntegerStringType::looseCompare() to behave like a numeric string.

Changes

  • src/Type/Accessory/AccessoryDecimalIntegerStringType.php: rewrote looseCompare().
    • Non-inverse (decimal-int-string): a decimal-int-string is a numeric, non-empty string, so it mirrors AccessoryNumericStringType::looseCompare()false only against null or a definitely non-numeric string, otherwise bool.
    • Inverse (non-decimal-int-string): always returns bool, since the value may be numeric ("02", "2.0", "1e1") or empty ("") and therefore can be loosely equal to a decimal-int-string, another numeric value, or null.
  • tests/PHPStan/Analyser/nsrt/bug-14793.php: new regression test.

Root cause

The old implementation assumed a decimal-int-string can never be loosely equal to a string that is not a decimal-int-string. That ignores PHP's numeric-string juggling: a non-decimal string such as "02", "2.0" or "1e1" is still numeric and compares numerically. The same flawed assumption applied symmetrically to the inverse non-decimal-int-string type, and the unconditional null short-circuit was wrong for the inverse type because "" is a valid non-decimal-int-string and '' == null is true.

I probed the parallel accessory string types on the same axis (loose comparison of refined string types): AccessoryNumericStringType, AccessoryNonEmptyStringType, and AccessoryLowercaseStringType/AccessoryUppercaseStringType already handle the numeric-string case correctly (the lowercase/uppercase variants explicitly fall back to bool when either side may be numeric), so no analogous fix was required there.

Test

tests/PHPStan/Analyser/nsrt/bug-14793.php asserts:

  • decimal-int-string == non-decimal-int-string (both directions), == numeric-string, == '2.0', == '02' infer bool (previously false).
  • decimal-int-string == 'foo' and == null still infer false (correct: non-numeric string / non-empty string vs null).
  • non-decimal-int-string == null infers bool (previously false), because '' == null is true.

Verified the test fails on the unpatched source (Expected: bool, Actual: false) and passes with the fix.

Fixes phpstan/phpstan#14793

…stead of assuming inequality with non-decimal strings

- `AccessoryDecimalIntegerStringType::looseCompare()` wrongly returned `false` whenever the other operand was a string that is not a `decimal-int-string`. Numeric strings compare numerically in PHP, so `'2' == '02'` and `'2' == '2.0'` are `true`.
- For the non-inverse case (an actual `decimal-int-string`), mirror `AccessoryNumericStringType::looseCompare()`: a decimal-int-string is a numeric, non-empty string, so it is never loosely equal to `null` or to a non-numeric string, but may equal any numeric value.
- For the inverse case (`non-decimal-int-string`), always return `bool`: such a value may still be numeric (`"02"`, `"2.0"`, `"1e1"`) or empty (`""`), so it can be loosely equal to a decimal-int-string, another numeric value, or `null` (`'' == null` is `true`).
- Probed sibling accessory string types: `AccessoryNumericStringType`, `AccessoryNonEmptyStringType`, `AccessoryLowercaseStringType`/`AccessoryUppercaseStringType` already guard numeric-string loose comparison correctly; no change needed there.
@staabm staabm requested a review from ondrejmirtes June 7, 2026 16:30
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.

A decimal-int-string may be loosely equal to a non-decimal-int-string

1 participant