diff --git a/conf/config.neon b/conf/config.neon index e764d064eb..1418a470e6 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1721,6 +1721,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\StrlenTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension tags: diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 52b3d76d45..c2659526de 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -331,46 +331,6 @@ public function specifyTypesInCondition( } } - if ( - !$context->null() - && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) >= 3 - && $expr->right->name instanceof Name - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), - $context, - )->setRootExpr($expr); - } - - if ( - !$context->null() - && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) === 1 - && $expr->right->name instanceof Name - && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) - && $leftType->isInteger()->yes() - ) { - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $accessory = new AccessoryNonEmptyStringType(); - - if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { - $accessory = new AccessoryNonFalsyStringType(); - } - - $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); - } - } - } - if ($leftType instanceof ConstantIntegerType) { if ($expr->right instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( @@ -466,6 +426,26 @@ public function specifyTypesInCondition( } } + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['preg_match', 'strlen', 'mb_strlen'], true) + ) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + $newScope = $scope->filterBySpecifiedTypes($result); + $callType = $newScope->getType($expr->right); + $newContext = $context->newWithReturnType($callType); + + $result = $result->unionWith($this->specifyTypesInCondition( + $scope, + $expr->right, + $newContext, + )->setRootExpr($expr)); + } + return $result; } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) { diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index fe09aa861c..4c32f56e89 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Type; /** * @api @@ -18,8 +19,7 @@ final class TypeSpecifierContext public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE; public const CONTEXT_BITMASK = 0b1111; - /** @var self[] */ - private static array $registry; + private ?Type $returnType = null; private function __construct(private ?int $value) { @@ -27,8 +27,7 @@ private function __construct(private ?int $value) private static function create(?int $value): self { - self::$registry[$value] ??= new self($value); - return self::$registry[$value]; + return new self($value); } public static function createTrue(): self @@ -89,4 +88,16 @@ public function null(): bool return $this->value === null; } + public function newWithReturnType(Type $type): self + { + $new = self::create($this->value); + $new->returnType = $type; + return $new; + } + + public function getReturnType(): ?Type + { + return $this->returnType; + } + } diff --git a/src/Type/Php/StrlenTypeSpecifyingExtension.php b/src/Type/Php/StrlenTypeSpecifyingExtension.php new file mode 100644 index 0000000000..4d5ebfa53e --- /dev/null +++ b/src/Type/Php/StrlenTypeSpecifyingExtension.php @@ -0,0 +1,72 @@ +typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return in_array(strtolower($functionReflection->getName()), ['strlen', 'mb_strlen'], true) && !$context->null(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + if ( + count($args) < 1 + || $context->getReturnType() === null + ) { + return new SpecifiedTypes(); + } + + $argType = $scope->getType($args[0]->value); + if (!$argType->isString()->yes()) { + return new SpecifiedTypes(); + } + + $returnType = $context->getReturnType(); + if ($returnType instanceof NeverType) { + return $this->typeSpecifier->create($args[0]->value, $returnType, $context->negate(), $scope); + } + + if ( + $context->true() && IntegerRangeType::createAllGreaterThanOrEqualTo(1)->isSuperTypeOf($returnType)->yes() + || ($context->false() && (new ConstantIntegerType(0))->isSuperTypeOf($returnType)->yes()) + ) { + $accessory = new AccessoryNonEmptyStringType(); + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2)->isSuperTypeOf($returnType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } + + return $this->typeSpecifier->create($args[0]->value, $accessory, $context, $scope)->setRootExpr($node); + } + + return new SpecifiedTypes(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/strlen-never.php b/tests/PHPStan/Analyser/nsrt/strlen-never.php new file mode 100644 index 0000000000..7f68b0a397 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/strlen-never.php @@ -0,0 +1,50 @@ += 0) { + assertType('non-empty-string', $nonES); + } else { + assertType('*NEVER*', $nonES); + } +} + + +function doBar(string $m): void +{ + if (strlen($m) >= 1) { + if (strlen($m) <= 0) { + assertType('*NEVER*', $m); + } + } +} +