From 55910d8fa8be25c177d00c2212285a1b50cea3ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 14 Mar 2025 09:19:37 +0100 Subject: [PATCH 1/5] Implement TypeSpecifierContext->getReturnType() --- src/Analyser/TypeSpecifier.php | 36 ++++++++++++++++----------- src/Analyser/TypeSpecifierContext.php | 15 +++++++++++ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 52b3d76d45..cac199711d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -331,21 +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 @@ -466,6 +451,27 @@ public function specifyTypesInCondition( } } + if ( + !$context->null() + && $expr->left instanceof Node\Scalar + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && in_array(strtolower((string) $expr->right->name), ['preg_match'], 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..f8f9c63891 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 @@ -21,6 +22,8 @@ final class TypeSpecifierContext /** @var self[] */ private static array $registry; + private ?Type $returnType = null; + private function __construct(private ?int $value) { } @@ -89,4 +92,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; + } + } From 79555497cb4b1b1721f2a54e1145b568e9b6f5af Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 14 Mar 2025 10:26:41 +0100 Subject: [PATCH 2/5] Refactor code into StrlenTypeSpecifyingExtension --- conf/config.neon | 5 ++ src/Analyser/TypeSpecifier.php | 28 +------- .../Php/StrlenTypeSpecifyingExtension.php | 67 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/narrow-cast.php | 4 +- 4 files changed, 75 insertions(+), 29 deletions(-) create mode 100644 src/Type/Php/StrlenTypeSpecifyingExtension.php 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 cac199711d..c2659526de 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -331,31 +331,6 @@ public function specifyTypesInCondition( } } - 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( @@ -453,10 +428,9 @@ public function specifyTypesInCondition( if ( !$context->null() - && $expr->left instanceof Node\Scalar && $expr->right instanceof Expr\FuncCall && $expr->right->name instanceof Name - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && in_array(strtolower((string) $expr->right->name), ['preg_match', 'strlen', 'mb_strlen'], true) ) { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); diff --git a/src/Type/Php/StrlenTypeSpecifyingExtension.php b/src/Type/Php/StrlenTypeSpecifyingExtension.php new file mode 100644 index 0000000000..9ca9da98ad --- /dev/null +++ b/src/Type/Php/StrlenTypeSpecifyingExtension.php @@ -0,0 +1,67 @@ +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 ( + $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/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index 5687c0b23a..b86b3781b2 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -7,9 +7,9 @@ /** @param array $arr */ function doFoo(string $x, array $arr): void { if ((bool) strlen($x)) { - assertType('string', $x); // could be non-empty-string + assertType('non-empty-string', $x); } else { - assertType('string', $x); + assertType("''", $x); } assertType('string', $x); From 93e22614aef01800178b64b57bcca07267946c27 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 14 Mar 2025 11:38:07 +0100 Subject: [PATCH 3/5] Discard changes to tests/PHPStan/Analyser/nsrt/narrow-cast.php --- tests/PHPStan/Analyser/nsrt/narrow-cast.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index b86b3781b2..5687c0b23a 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -7,9 +7,9 @@ /** @param array $arr */ function doFoo(string $x, array $arr): void { if ((bool) strlen($x)) { - assertType('non-empty-string', $x); + assertType('string', $x); // could be non-empty-string } else { - assertType("''", $x); + assertType('string', $x); } assertType('string', $x); From 87603dc9eb0e5a6c99c43582b4dbe53c6463e314 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 14 Mar 2025 11:51:23 +0100 Subject: [PATCH 4/5] Remove TypeSpecifierContext caching --- src/Analyser/TypeSpecifierContext.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index f8f9c63891..4c32f56e89 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -19,9 +19,6 @@ 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) @@ -30,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 From 3212ca9113d017a9bc74bfc230a4bf2018f0af7e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 15 Mar 2025 09:54:41 +0100 Subject: [PATCH 5/5] fix --- .../Php/StrlenTypeSpecifyingExtension.php | 5 ++ tests/PHPStan/Analyser/nsrt/strlen-never.php | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/strlen-never.php diff --git a/src/Type/Php/StrlenTypeSpecifyingExtension.php b/src/Type/Php/StrlenTypeSpecifyingExtension.php index 9ca9da98ad..4d5ebfa53e 100644 --- a/src/Type/Php/StrlenTypeSpecifyingExtension.php +++ b/src/Type/Php/StrlenTypeSpecifyingExtension.php @@ -14,6 +14,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\NeverType; use function count; use function in_array; use function strtolower; @@ -49,6 +50,10 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } $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()) 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); + } + } +} +