From 1918af82432169f5b114b6b8ac76c763bd3ce5e6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 28 Mar 2026 16:08:43 +0100 Subject: [PATCH 1/8] ctype_digit() narrows to decimal-int-string --- src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php | 5 +++-- tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php | 7 ++++--- tests/PHPStan/Analyser/nsrt/ctype-digit.php | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index 04daf2ed552..baa2e29d788 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -12,6 +12,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; @@ -56,7 +57,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ($context->true()) { $types[] = new IntersectionType([ new StringType(), - new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(), ]); } @@ -68,7 +69,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n IntegerRangeType::fromInterval(0, null), new IntersectionType([ new StringType(), - new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(), ]), new ConstantBooleanType(true), ]); diff --git a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php index 239e49d53a9..e1225bf3197 100644 --- a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php +++ b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php @@ -13,7 +13,7 @@ class HelloWorld public function sayHello($mixed, int $int, string $string, $numericString, $nonEmptyString, bool $bool): void { if (ctype_digit((string) $mixed)) { - assertType('int<0, max>|numeric-string|true', $mixed); + assertType('int<0, max>|decimal-int-string|true', $mixed); } else { assertType('mixed~(int<0, max>|numeric-string|true)', $mixed); } @@ -41,7 +41,7 @@ public function sayHello($mixed, int $int, string $string, $numericString, $nonE assertType('int', $int); if (ctype_digit((string) $string)) { - assertType('numeric-string', $string); + assertType('decimal-int-string', $string); } else { assertType('string', $string); } @@ -54,10 +54,11 @@ public function sayHello($mixed, int $int, string $string, $numericString, $nonE } assertType('string', $string); + // see https://3v4l.org/1Qrlg#veol if (ctype_digit((string) $numericString)) { assertType('numeric-string', $numericString); } else { - assertType('*NEVER*', $numericString); + assertType('numeric-string', $numericString); } assertType('numeric-string', $numericString); diff --git a/tests/PHPStan/Analyser/nsrt/ctype-digit.php b/tests/PHPStan/Analyser/nsrt/ctype-digit.php index ed4704daa71..ec496e6fbe1 100644 --- a/tests/PHPStan/Analyser/nsrt/ctype-digit.php +++ b/tests/PHPStan/Analyser/nsrt/ctype-digit.php @@ -14,7 +14,7 @@ public function foo(mixed $foo): void assertType('mixed', $foo); if (is_string($foo) && ctype_digit($foo)) { - assertType('numeric-string', $foo); + assertType('decimal-int-string', $foo); } else { assertType('mixed', $foo); } @@ -26,7 +26,7 @@ public function foo(mixed $foo): void } if (ctype_digit($foo)) { - assertType('int<48, 57>|int<256, max>|numeric-string', $foo); + assertType('int<48, 57>|int<256, max>|decimal-int-string', $foo); return; } From b623a162095a71449de1398d302101e5d2957a22 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 28 Mar 2026 16:11:09 +0100 Subject: [PATCH 2/8] Update CtypeDigitFunctionTypeSpecifyingExtension.php --- src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index baa2e29d788..cab2d7d007d 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -13,7 +13,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerRangeType; From beb586e1fc7fc9fcb4ac7cd9c03715b237902cb5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 28 Mar 2026 16:20:26 +0100 Subject: [PATCH 3/8] Update TypeCombinatorTest.php --- tests/PHPStan/Type/TypeCombinatorTest.php | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 149d6fcbdb8..b652e0139d8 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -5713,6 +5713,30 @@ public static function dataRemove(): array UnionType::class, 'array|ArrayObject', ], + [ + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]), + IntersectionType::class, + 'numeric-string' + ], + [ + new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + NeverType::class, + '*NEVER*=implicit' + ], [ new ConstantBooleanType(true), new ConstantBooleanType(false), From 1d21825e5d14dccab71ec4468b274c0216c47e32 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 28 Mar 2026 16:27:39 +0100 Subject: [PATCH 4/8] Update callsite-cast-narrowing.php --- tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php index e1225bf3197..9dc8c720674 100644 --- a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php +++ b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php @@ -56,7 +56,7 @@ public function sayHello($mixed, int $int, string $string, $numericString, $nonE // see https://3v4l.org/1Qrlg#veol if (ctype_digit((string) $numericString)) { - assertType('numeric-string', $numericString); + assertType('decimal-int-string', $numericString); } else { assertType('numeric-string', $numericString); } From 06639455eae210bfc01d19e99d27ce3a81368790 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 2 Jun 2026 10:58:44 +0200 Subject: [PATCH 5/8] Update TypeCombinatorTest.php --- tests/PHPStan/Type/TypeCombinatorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index b652e0139d8..ed02cc51e3a 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -5723,7 +5723,7 @@ public static function dataRemove(): array new AccessoryDecimalIntegerStringType(), ]), IntersectionType::class, - 'numeric-string' + 'numeric-string', ], [ new IntersectionType([ @@ -5735,7 +5735,7 @@ public static function dataRemove(): array new AccessoryNumericStringType(), ]), NeverType::class, - '*NEVER*=implicit' + '*NEVER*=implicit', ], [ new ConstantBooleanType(true), From 17c498db2321f69a12f637e8a01ba4eb26f6ce30 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 2 Jun 2026 11:27:25 +0200 Subject: [PATCH 6/8] fix --- .../CtypeDigitFunctionTypeSpecifyingExtension.php | 13 +++++++++---- .../Analyser/nsrt/callsite-cast-narrowing.php | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index cab2d7d007d..746892926cd 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -13,6 +13,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerRangeType; @@ -64,12 +65,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, $scope); if ($exprArg instanceof Cast\String_) { + $accessories = [ + new StringType(), + new AccessoryNumericStringType() + ]; + if ($context->true()) { + $accessories[] = new AccessoryDecimalIntegerStringType(); + } $castedType = new UnionType([ IntegerRangeType::fromInterval(0, null), - new IntersectionType([ - new StringType(), - new AccessoryDecimalIntegerStringType(), - ]), + new IntersectionType($accessories), new ConstantBooleanType(true), ]); $specifiedTypes = $specifiedTypes->unionWith( diff --git a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php index 9dc8c720674..7665cbe2c22 100644 --- a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php +++ b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php @@ -56,9 +56,9 @@ public function sayHello($mixed, int $int, string $string, $numericString, $nonE // see https://3v4l.org/1Qrlg#veol if (ctype_digit((string) $numericString)) { - assertType('decimal-int-string', $numericString); - } else { assertType('numeric-string', $numericString); + } else { + assertType('*NEVER*', $numericString); } assertType('numeric-string', $numericString); From f874f03748c004fe7eba629c191d5384db6b4b1b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 2 Jun 2026 11:29:49 +0200 Subject: [PATCH 7/8] Update CtypeDigitFunctionTypeSpecifyingExtension.php --- src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index 746892926cd..bc44dd244f4 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -67,7 +67,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ($exprArg instanceof Cast\String_) { $accessories = [ new StringType(), - new AccessoryNumericStringType() + new AccessoryNumericStringType(), ]; if ($context->true()) { $accessories[] = new AccessoryDecimalIntegerStringType(); From cbac7fea93735406e5af06a966b924d132956c86 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 2 Jun 2026 16:18:27 +0200 Subject: [PATCH 8/8] test identical true --- .../PHPStan/Analyser/nsrt/callsite-cast-narrowing.php | 7 +++++++ tests/PHPStan/Analyser/nsrt/ctype-digit.php | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php index 7665cbe2c22..d589cb513d0 100644 --- a/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php +++ b/tests/PHPStan/Analyser/nsrt/callsite-cast-narrowing.php @@ -68,6 +68,13 @@ public function sayHello($mixed, int $int, string $string, $numericString, $nonE assertType('false', $bool); } assertType('bool', $bool); + + if (ctype_digit((string) $bool) === true) { + assertType('true', $bool); + } else { + assertType('false', $bool); + } + assertType('bool', $bool); } } diff --git a/tests/PHPStan/Analyser/nsrt/ctype-digit.php b/tests/PHPStan/Analyser/nsrt/ctype-digit.php index ec496e6fbe1..29ae49ee95d 100644 --- a/tests/PHPStan/Analyser/nsrt/ctype-digit.php +++ b/tests/PHPStan/Analyser/nsrt/ctype-digit.php @@ -32,4 +32,14 @@ public function foo(mixed $foo): void assertType('mixed~(int<48, 57>|int<256, max>)', $foo); // not all numeric strings are covered by ctype_digit } + + public function doString(string $string): void + { + if (ctype_digit($string) === true) { + assertType('decimal-int-string', $string); + } else { + assertType('string', $string); + } + + } }