diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 356d0b30be..ea1ceebcde 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1200,41 +1200,6 @@ private function specifyTypesForConstantStringBinaryExpression( } $constantStringValue = $scalarValues[0]; - if ( - $context->truthy() - && $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && in_array(strtolower($exprNode->name->toString()), [ - 'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst', - 'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst', - 'ucwords', 'mb_convert_case', 'mb_convert_kana', - ], true) - && isset($exprNode->getArgs()[0]) - && $constantStringValue !== '' - ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); - - if ($argType->isString()->yes()) { - if ($constantStringValue !== '0') { - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), - $context, - false, - $scope, - ); - } - - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), - $context, - false, - $scope, - ); - } - } - if ( $exprNode instanceof FuncCall && $exprNode->name instanceof Name @@ -2171,6 +2136,41 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + if ( + $context->truthy() + && $unwrappedLeftExpr instanceof FuncCall + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower($unwrappedLeftExpr->name->toString()), [ + 'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst', + 'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst', + 'ucwords', 'mb_convert_case', 'mb_convert_kana', + ], true) + && isset($unwrappedLeftExpr->getArgs()[0]) + && $rightType->isNonEmptyString()->yes() + ) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + + if ($argType->isString()->yes()) { + if ($rightType->isNonFalsyString()->yes()) { + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), + $context, + false, + $scope, + ); + } + + return $this->create( + $unwrappedLeftExpr->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), + $context, + false, + $scope, + ); + } + } + if ($rightType->isInteger()->yes() || $rightType->isString()->yes()) { $types = null; foreach ($rightType->getFiniteTypes() as $finiteType) { diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php index 6307af45e9..8ad670a7f8 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php @@ -4,7 +4,8 @@ use function PHPStan\Testing\assertType; -class Foo { +class Foo +{ public function nonEmptySubstr(string $s, int $offset, int $length): void { if (substr($s, 10) === 'hallo') { @@ -81,4 +82,19 @@ public function nonEmptySubstr(string $s, int $offset, int $length): void assertType('\'hallo\'', $x); } } + + /** + * @param non-empty-string $nonES + * @param non-falsy-string $falsyString + */ + public function stringTypes(string $s, $nonES, $falsyString): void + { + if (substr($s, 10) === $nonES) { + assertType('non-empty-string', $s); + } + + if (substr($s, 10) === $falsyString) { + assertType('non-falsy-string', $s); + } + } }