From e3ce1eee4b70b4a4529aff210612d0b439f181bf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Jan 2023 10:06:46 +0100 Subject: [PATCH 1/2] Support type narrowing with casted parameters --- ...peDigitFunctionTypeSpecifyingExtension.php | 17 ++++++++-- .../Analyser/NodeScopeResolverTest.php | 2 ++ .../Analyser/data/callsite-cast-narrowing.php | 33 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/callsite-cast-narrowing.php diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index ab1bd7aebd..8178153214 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\Cast; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -38,7 +39,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n throw new ShouldNotHappenException(); } - if ($context->true() && $scope->getType($node->getArgs()[0]->value)->isNumericString()->yes()) { + $exprArg = $node->getArgs()[0]->value; + if ($context->true() && $scope->getType($exprArg)->isNumericString()->yes()) { return new SpecifiedTypes(); } @@ -54,7 +56,18 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]); } - return $this->typeSpecifier->create($node->getArgs()[0]->value, TypeCombinator::union(...$types), $context, false, $scope); + $unionType = TypeCombinator::union(...$types); + $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, false, $scope); + + if ($context->true() && $exprArg instanceof Cast) { + $castOriginType = TypeCombinator::intersect($scope->getType($exprArg->expr), $unionType); + + $specifiedTypes = $specifiedTypes->unionWith( + $this->typeSpecifier->create($exprArg->expr, $castOriginType, $context, false, $scope) + ); + } + + return $specifiedTypes; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6b8cbe3a03..49878908f4 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1175,6 +1175,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8621.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8084.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3019.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php'); } /** diff --git a/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php new file mode 100644 index 0000000000..a4b585d6f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php @@ -0,0 +1,33 @@ +|int<256, max>|numeric-string', $mixed); + } + if (ctype_digit((int) $mixed)) { + assertType('int<48, 57>|int<256, max>|numeric-string', $mixed); + } + + if (ctype_digit((string) $int)) { + assertType('int', $int); + } + if (ctype_digit((int) $int)) { + assertType('int<48, 57>|int<256, max>', $int); + } + + if (ctype_digit((string) $string)) { + assertType('numeric-string', $string); + } + if (ctype_digit((int) $string)) { + assertType('numeric-string', $string); + } + } + +} From 683dcb1c8bbd1bf971dd0813c361941929bada73 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 17 Jan 2023 18:07:42 +0100 Subject: [PATCH 2/2] more tests and impl simplified --- ...peDigitFunctionTypeSpecifyingExtension.php | 4 +--- .../Analyser/data/callsite-cast-narrowing.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php index 8178153214..ec4a59d5f3 100644 --- a/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CtypeDigitFunctionTypeSpecifyingExtension.php @@ -60,10 +60,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $specifiedTypes = $this->typeSpecifier->create($exprArg, $unionType, $context, false, $scope); if ($context->true() && $exprArg instanceof Cast) { - $castOriginType = TypeCombinator::intersect($scope->getType($exprArg->expr), $unionType); - $specifiedTypes = $specifiedTypes->unionWith( - $this->typeSpecifier->create($exprArg->expr, $castOriginType, $context, false, $scope) + $this->typeSpecifier->create($exprArg->expr, $unionType, $context, false, $scope), ); } diff --git a/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php b/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php index a4b585d6f4..576409530e 100644 --- a/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php +++ b/tests/PHPStan/Analyser/data/callsite-cast-narrowing.php @@ -10,24 +10,45 @@ public function sayHello($mixed, int $int, string $string): void { if (ctype_digit((string) $mixed)) { assertType('int<48, 57>|int<256, max>|numeric-string', $mixed); + } else { + assertType('mixed', $mixed); } + assertType('mixed', $mixed); + if (ctype_digit((int) $mixed)) { assertType('int<48, 57>|int<256, max>|numeric-string', $mixed); + } else { + assertType('mixed', $mixed); } + assertType('mixed', $mixed); if (ctype_digit((string) $int)) { assertType('int', $int); + } else { + assertType('int', $int); } + assertType('int', $int); + if (ctype_digit((int) $int)) { assertType('int<48, 57>|int<256, max>', $int); + } else { + assertType('int', $int); } + assertType('int', $int); if (ctype_digit((string) $string)) { assertType('numeric-string', $string); + } else { + assertType('string', $string); } + assertType('string', $string); + if (ctype_digit((int) $string)) { assertType('numeric-string', $string); + } else { + assertType('string', $string); } + assertType('string', $string); } }