From ec7d51003b8e94568a6df8ccdb794b919dcef677 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Sep 2025 16:48:55 +0200 Subject: [PATCH 1/4] Narrow l/r/trim() arg on comparison to '' --- src/Analyser/TypeSpecifier.php | 20 ++++++ tests/PHPStan/Analyser/nsrt/bug-12973.php | 68 +++++++++++++++++++ .../Rules/Functions/ReturnTypeRuleTest.php | 7 ++ 3 files changed, 95 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12973.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index bcb9dafae0..4b32ebae7a 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1303,6 +1303,26 @@ private function specifyTypesForConstantStringBinaryExpression( )->setRootExpr($rootExpr); } + if ( + $context->false() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && in_array(strtolower((string) $exprNode->name), ['trim', 'ltrim', 'rtrim'], true) + && isset($exprNode->getArgs()[0]) + && $constantStringValue === '' + ) { + $argType = $scope->getType($exprNode->getArgs()[0]->value); + + if ($argType->isString()->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + new AccessoryNonEmptyStringType(), + $context->negate(), + $scope, + )->setRootExpr($rootExpr); + } + } + return null; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12973.php b/tests/PHPStan/Analyser/nsrt/bug-12973.php new file mode 100644 index 0000000000..8359fda89d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12973.php @@ -0,0 +1,68 @@ +checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12973.php'], []); + } + } From 6b49219348b44c484232b556a9d592965415071f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 18 Sep 2025 06:39:09 +0200 Subject: [PATCH 2/4] trim MixedType --- src/Analyser/TypeSpecifier.php | 15 ++++++++------- tests/PHPStan/Analyser/nsrt/bug-12973.php | 11 +++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4b32ebae7a..2549ce5827 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1313,14 +1313,15 @@ private function specifyTypesForConstantStringBinaryExpression( ) { $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isString()->yes()) { - return $this->create( - $exprNode->getArgs()[0]->value, + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::intersect( + new StringType(), new AccessoryNonEmptyStringType(), - $context->negate(), - $scope, - )->setRootExpr($rootExpr); - } + ), + $context->negate(), + $scope, + )->setRootExpr($rootExpr); } return null; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12973.php b/tests/PHPStan/Analyser/nsrt/bug-12973.php index 8359fda89d..14116eef93 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12973.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12973.php @@ -16,6 +16,17 @@ function nullIfBlank(?string $value): ?string return null; } +function trimMixed($value): void +{ + if (trim($value) === '') { + assertType('mixed', $value); + } else { + assertType('non-empty-string', $value); + } + assertType('mixed', $value); + +} + function trimTypes(string $value): void { if (trim($value) === '') { From 0cc03f612d1728a5bcf1f5c1c4e470c4ec76cd8b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 18 Sep 2025 06:56:48 +0200 Subject: [PATCH 3/4] fix lint --- src/Analyser/TypeSpecifier.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 2549ce5827..31edaf5d82 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1311,8 +1311,6 @@ private function specifyTypesForConstantStringBinaryExpression( && isset($exprNode->getArgs()[0]) && $constantStringValue === '' ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); - return $this->create( $exprNode->getArgs()[0]->value, TypeCombinator::intersect( From b35db468c8497d538ee00b356f221cc9bb0af135 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 18 Sep 2025 06:59:36 +0200 Subject: [PATCH 4/4] support mb_r/l/trim --- src/Analyser/TypeSpecifier.php | 5 +++- .../PHPStan/Analyser/nsrt/bug-12973-php84.php | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12973-php84.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 31edaf5d82..159a523599 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1307,7 +1307,10 @@ private function specifyTypesForConstantStringBinaryExpression( $context->false() && $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['trim', 'ltrim', 'rtrim'], true) + && in_array(strtolower((string) $exprNode->name), [ + 'trim', 'ltrim', 'rtrim', + 'mb_trim', 'mb_ltrim', 'mb_rtrim', + ], true) && isset($exprNode->getArgs()[0]) && $constantStringValue === '' ) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php new file mode 100644 index 0000000000..cfbe20ce83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12973-php84.php @@ -0,0 +1,29 @@ += 8.4 + +namespace Bug12973Php84; + +use function PHPStan\Testing\assertType; + +function mbtrim($value): void +{ + if (mb_trim($value) === '') { + assertType('mixed', $value); + } else { + assertType('non-empty-string', $value); + } + assertType('mixed', $value); + + if (mb_ltrim($value) === '') { + assertType('mixed', $value); + } else { + assertType('non-empty-string', $value); + } + assertType('mixed', $value); + + if (mb_rtrim($value) === '') { + assertType('mixed', $value); + } else { + assertType('non-empty-string', $value); + } + assertType('mixed', $value); +}