From 28d3ced39a78ae8faaf6f64e9f44c3cf2e1e3151 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Mon, 9 Nov 2026 11:09:51 +0100 Subject: [PATCH] Expand character ranges when trimming a numeric-string Co-Authored-By: Claude Opus 5.5 --- .../Php/LtrimFunctionReturnTypeExtension.php | 9 +++- src/Type/Php/TrimCharactersHelper.php | 44 +++++++++++++++++++ ...TrimFunctionDynamicReturnTypeExtension.php | 9 +++- .../Analyser/nsrt/numeric-string-trim.php | 10 +++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/Type/Php/TrimCharactersHelper.php diff --git a/src/Type/Php/LtrimFunctionReturnTypeExtension.php b/src/Type/Php/LtrimFunctionReturnTypeExtension.php index 9cf4b23cf64..fccf0521c53 100644 --- a/src/Type/Php/LtrimFunctionReturnTypeExtension.php +++ b/src/Type/Php/LtrimFunctionReturnTypeExtension.php @@ -18,12 +18,17 @@ use PHPStan\Type\TypeCombinator; use function count; use function ltrim; -use function preg_match; #[AutowiredService] final class LtrimFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct( + private TrimCharactersHelper $trimCharactersHelper, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'ltrim'; @@ -72,7 +77,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($trimConstantString->getValue() === '\\' && $string->isClassString()->yes()) { $result[] = new ClassStringType(); - } elseif (preg_match('/\d/', $trimConstantString->getValue()) === 0 && $string->isNumericString()->yes()) { + } elseif (!$this->trimCharactersHelper->containsDigit($trimConstantString->getValue()) && $string->isNumericString()->yes()) { $result[] = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); } else { return $defaultType; diff --git a/src/Type/Php/TrimCharactersHelper.php b/src/Type/Php/TrimCharactersHelper.php new file mode 100644 index 00000000000..cba5fd278fb --- /dev/null +++ b/src/Type/Php/TrimCharactersHelper.php @@ -0,0 +1,44 @@ += $byte + ) { + if ($byte <= ord('9') && ord($characters[$i + 3]) >= ord('0')) { + return true; + } + + $i += 3; + continue; + } + + if ($byte >= ord('0') && $byte <= ord('9')) { + return true; + } + } + + return false; + } + +} diff --git a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php index 4a88a6616f0..e658b6df12d 100644 --- a/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/TrimFunctionDynamicReturnTypeExtension.php @@ -17,7 +17,6 @@ use PHPStan\Type\TypeCombinator; use function count; use function in_array; -use function preg_match; use function rtrim; use function trim; @@ -25,6 +24,12 @@ final class TrimFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct( + private TrimCharactersHelper $trimCharactersHelper, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['trim', 'rtrim', 'chop'], true); @@ -77,7 +82,7 @@ public function getTypeFromFunctionCall( true, ); } - } elseif (preg_match('/\d/', $trimConstantString->getValue()) === 0 && $stringType->isNumericString()->yes()) { + } elseif (!$this->trimCharactersHelper->containsDigit($trimConstantString->getValue()) && $stringType->isNumericString()->yes()) { $result[] = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); } else { return $defaultType; diff --git a/tests/PHPStan/Analyser/nsrt/numeric-string-trim.php b/tests/PHPStan/Analyser/nsrt/numeric-string-trim.php index 9a35224ab06..359f0f9f917 100644 --- a/tests/PHPStan/Analyser/nsrt/numeric-string-trim.php +++ b/tests/PHPStan/Analyser/nsrt/numeric-string-trim.php @@ -28,6 +28,16 @@ public function doTrim(string $numericString, string $string): void assertType('string', trim($numericString, '-0')); assertType('string', ltrim($numericString, '-0')); assertType('string', rtrim($numericString, '-0')); + + assertType('string', trim($numericString, '/..:')); + assertType('string', ltrim($numericString, '/..:')); + assertType('string', rtrim($numericString, '/..:')); + assertType('string', chop($numericString, '/..:')); + assertType('string', trim($numericString, '!..~')); + + assertType('numeric-string', trim($numericString, 'a..z')); + assertType('numeric-string', trim($numericString, '..:')); + assertType('numeric-string', trim($numericString, ':../')); } }