diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index f02e5f51d2..9f23cf6472 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1124,39 +1124,6 @@ private function specifyTypesForConstantBinaryExpression( )); } - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['strlen', 'mb_strlen'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($constantType->getValue() < 0) { - return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); - } - - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - - $accessory = new AccessoryNonEmptyStringType(); - if ($constantType->getValue() >= 2) { - $accessory = new AccessoryNonFalsyStringType(); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr); - - return $funcTypes->unionWith($valueTypes); - } - } - - } - return null; } @@ -2140,6 +2107,42 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } + if ( + !$context->null() + && $unwrappedLeftExpr instanceof FuncCall + && count($unwrappedLeftExpr->getArgs()) === 1 + && $unwrappedLeftExpr->name instanceof Name + && in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true) + && $rightType->isInteger()->yes() + ) { + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) { + return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr); + } + + $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); + if ($isZero->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + return $funcTypes->unionWith( + $this->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), $context, false, $scope, $rootExpr), + ); + } + + if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { + $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr); + + $accessory = new AccessoryNonEmptyStringType(); + if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + $valueTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, $context, false, $scope, $rootExpr); + + return $funcTypes->unionWith($valueTypes); + } + } + } + if ( $context->true() && $unwrappedLeftExpr instanceof FuncCall @@ -2209,14 +2212,11 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty } } - if ($rightType->isInteger()->yes() || $rightType->isString()->yes()) { + if ($rightType->isString()->yes()) { $types = null; - foreach ($rightType->getFiniteTypes() as $finiteType) { - if ($finiteType->isString()->yes()) { - $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr); - } else { - $specifiedType = $this->specifyTypesForConstantBinaryExpression($unwrappedLeftExpr, $finiteType, $context, $scope, $rootExpr); - } + foreach ($rightType->getConstantStrings() as $constantString) { + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $rootExpr); + if ($specifiedType === null) { continue; } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index c2b21e92c4..37fbd12ccf 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1283,7 +1283,7 @@ public function dataCondition(): iterable ), ), [ - '$foo' => 'non-empty-string', + '$foo' => "string & ~''", 'strlen($foo)' => '~0', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php index f66d50c140..f83e19e984 100644 --- a/tests/PHPStan/Analyser/nsrt/strlen-int-range.php +++ b/tests/PHPStan/Analyser/nsrt/strlen-int-range.php @@ -69,10 +69,10 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, } if (strlen($s) == $oneOrMore) { - assertType('string', $s); // could be non-empty-string + assertType('non-empty-string', $s); } if (strlen($s) === $oneOrMore) { - assertType('string', $s); // could be non-empty-string + assertType('non-empty-string', $s); } if (strlen($s) == $tenOrEleven) { @@ -118,7 +118,7 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree, * @param int<1, max> $oneOrMore * @param int<2, max> $twoOrMore */ -function doFooBar(array $arr, int $oneOrMore, int $twoOrMore): void +function doFooBar(string $s, array $arr, int $oneOrMore, int $twoOrMore): void { if (count($arr) == $oneOrMore) { assertType('non-empty-array', $arr); @@ -126,4 +126,12 @@ function doFooBar(array $arr, int $oneOrMore, int $twoOrMore): void if (count($arr) === $twoOrMore) { assertType('non-empty-array', $arr); } + + if (strlen($s) == $twoOrMore) { + assertType('non-falsy-string', $s); + } + if (strlen($s) === $twoOrMore) { + assertType('non-falsy-string', $s); + } + }