From 6d9684c04d2d6146799b0bd86b77bd4b91941172 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:20:31 +0000 Subject: [PATCH] Treat `decimal-int-string` as a numeric string in `looseCompare()` instead 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. --- .../AccessoryDecimalIntegerStringType.php | 21 ++++++---- tests/PHPStan/Analyser/nsrt/bug-14793.php | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14793.php diff --git a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php index d965eb0ebc..98c6b2d7bb 100644 --- a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php +++ b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php @@ -411,18 +411,23 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($this->inverse) { + // A non-decimal-int-string 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. Nothing can be decided here. + return new BooleanType(); + } + + // A decimal-int-string is a numeric string, so it compares like one: + // numerically against other numeric strings (and numbers), and never + // loosely equal to null (it is always non-empty) or to a non-numeric + // string (that would be a byte-wise comparison that cannot match). if ($type->isNull()->yes()) { return new ConstantBooleanType(false); } - if ($type->isString()->yes()) { - if ($this->inverse) { - if ($type->isDecimalIntegerString()->yes()) { - return new ConstantBooleanType(false); - } - } elseif ($type->isDecimalIntegerString()->no()) { - return new ConstantBooleanType(false); - } + if ($type->isString()->yes() && $type->isNumericString()->no()) { + return new ConstantBooleanType(false); } return new BooleanType(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14793.php b/tests/PHPStan/Analyser/nsrt/bug-14793.php new file mode 100644 index 0000000000..9c7c7c4154 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14793.php @@ -0,0 +1,40 @@ +