From 28c187f59d0965fd82ac78cf0738124120b1743c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 30 Nov 2025 12:21:13 +0100 Subject: [PATCH] Narrow types after cast in generator based Handlers --- .../Generator/ExprHandler/CastBoolHandler.php | 10 +-- .../ExprHandler/CastDoubleHandler.php | 10 ++- .../Generator/ExprHandler/CastIntHandler.php | 10 ++- .../ExprHandler/CastStringHandler.php | 10 ++- .../PHPStan/Analyser/Generator/data/gnsr.php | 61 +++++++++++++++++++ 5 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/Analyser/Generator/ExprHandler/CastBoolHandler.php b/src/Analyser/Generator/ExprHandler/CastBoolHandler.php index 5fd5c8a672..1bcd71f73a 100644 --- a/src/Analyser/Generator/ExprHandler/CastBoolHandler.php +++ b/src/Analyser/Generator/ExprHandler/CastBoolHandler.php @@ -4,13 +4,14 @@ use Generator; use PhpParser\Node\Expr; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\Generator\ExprAnalysisRequest; use PHPStan\Analyser\Generator\ExprAnalysisResult; use PHPStan\Analyser\Generator\ExprHandler; use PHPStan\Analyser\Generator\GeneratorScope; -use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\Generator\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; /** @@ -34,6 +35,7 @@ public function analyseExpr( ): Generator { $exprResult = yield new ExprAnalysisRequest($stmt, $expr->expr, $scope, $context->enterDeep(), $alternativeNodeCallback); + $specifiedExprResult = yield new ExprAnalysisRequest($stmt, new Expr\BinaryOp\Equal($expr->expr, new Expr\ConstFetch(new FullyQualified('true'))), $scope, $context->enterDeep(), new NoopNodeCallback()); return new ExprAnalysisResult( $exprResult->type->toBoolean(), @@ -43,9 +45,9 @@ public function analyseExpr( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - specifiedTruthyTypes: new SpecifiedTypes(), - specifiedFalseyTypes: new SpecifiedTypes(), - specifiedNullTypes: new SpecifiedTypes(), + specifiedTruthyTypes: $specifiedExprResult->specifiedTruthyTypes, + specifiedFalseyTypes: $specifiedExprResult->specifiedFalseyTypes, + specifiedNullTypes: $specifiedExprResult->specifiedNullTypes, ); } diff --git a/src/Analyser/Generator/ExprHandler/CastDoubleHandler.php b/src/Analyser/Generator/ExprHandler/CastDoubleHandler.php index e702813628..5f35aab8ef 100644 --- a/src/Analyser/Generator/ExprHandler/CastDoubleHandler.php +++ b/src/Analyser/Generator/ExprHandler/CastDoubleHandler.php @@ -4,12 +4,15 @@ use Generator; use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\DNumber; +use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\Generator\ExprAnalysisRequest; use PHPStan\Analyser\Generator\ExprAnalysisResult; use PHPStan\Analyser\Generator\ExprHandler; use PHPStan\Analyser\Generator\GeneratorScope; +use PHPStan\Analyser\Generator\NoopNodeCallback; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\DependencyInjection\AutowiredService; @@ -34,6 +37,7 @@ public function analyseExpr( ): Generator { $exprResult = yield new ExprAnalysisRequest($stmt, $expr->expr, $scope, $context->enterDeep(), $alternativeNodeCallback); + $specifiedExprResult = yield new ExprAnalysisRequest($stmt, new Expr\BinaryOp\Equal($expr->expr, new DNumber(0.0)), $scope, $context->enterDeep(), new NoopNodeCallback()); return new ExprAnalysisResult( $exprResult->type->toFloat(), @@ -43,9 +47,9 @@ public function analyseExpr( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - specifiedTruthyTypes: new SpecifiedTypes(), - specifiedFalseyTypes: new SpecifiedTypes(), - specifiedNullTypes: new SpecifiedTypes(), + specifiedTruthyTypes: $specifiedExprResult->specifiedTruthyTypes, + specifiedFalseyTypes: $specifiedExprResult->specifiedFalseyTypes, + specifiedNullTypes: $specifiedExprResult->specifiedNullTypes, ); } diff --git a/src/Analyser/Generator/ExprHandler/CastIntHandler.php b/src/Analyser/Generator/ExprHandler/CastIntHandler.php index 1b018c2698..f5fc45dbf4 100644 --- a/src/Analyser/Generator/ExprHandler/CastIntHandler.php +++ b/src/Analyser/Generator/ExprHandler/CastIntHandler.php @@ -5,12 +5,15 @@ use Generator; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Cast\Int_; +use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\Generator\ExprAnalysisRequest; use PHPStan\Analyser\Generator\ExprAnalysisResult; use PHPStan\Analyser\Generator\ExprHandler; use PHPStan\Analyser\Generator\GeneratorScope; +use PHPStan\Analyser\Generator\NoopNodeCallback; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\DependencyInjection\AutowiredService; @@ -35,6 +38,7 @@ public function analyseExpr( ): Generator { $exprResult = yield new ExprAnalysisRequest($stmt, $expr->expr, $scope, $context->enterDeep(), $alternativeNodeCallback); + $specifiedExprResult = yield new ExprAnalysisRequest($stmt, new Expr\BinaryOp\Equal($expr->expr, new LNumber(0)), $scope, $context->enterDeep(), new NoopNodeCallback()); return new ExprAnalysisResult( $exprResult->type->toInteger(), @@ -44,9 +48,9 @@ public function analyseExpr( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - specifiedTruthyTypes: new SpecifiedTypes(), - specifiedFalseyTypes: new SpecifiedTypes(), - specifiedNullTypes: new SpecifiedTypes(), + specifiedTruthyTypes: $specifiedExprResult->specifiedTruthyTypes, + specifiedFalseyTypes: $specifiedExprResult->specifiedFalseyTypes, + specifiedNullTypes: $specifiedExprResult->specifiedNullTypes, ); } diff --git a/src/Analyser/Generator/ExprHandler/CastStringHandler.php b/src/Analyser/Generator/ExprHandler/CastStringHandler.php index 766e4d9ad8..752b1f6ad4 100644 --- a/src/Analyser/Generator/ExprHandler/CastStringHandler.php +++ b/src/Analyser/Generator/ExprHandler/CastStringHandler.php @@ -4,12 +4,15 @@ use Generator; use PhpParser\Node\Expr; +use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\Generator\ExprAnalysisRequest; use PHPStan\Analyser\Generator\ExprAnalysisResult; use PHPStan\Analyser\Generator\ExprHandler; use PHPStan\Analyser\Generator\GeneratorScope; +use PHPStan\Analyser\Generator\NoopNodeCallback; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\DependencyInjection\AutowiredService; @@ -34,6 +37,7 @@ public function analyseExpr( ): Generator { $exprResult = yield new ExprAnalysisRequest($stmt, $expr->expr, $scope, $context->enterDeep(), $alternativeNodeCallback); + $specifiedExprResult = yield new ExprAnalysisRequest($stmt, new Expr\BinaryOp\Equal($expr->expr, new String_('')), $scope, $context->enterDeep(), new NoopNodeCallback()); return new ExprAnalysisResult( $exprResult->type->toString(), @@ -43,9 +47,9 @@ public function analyseExpr( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - specifiedTruthyTypes: new SpecifiedTypes(), - specifiedFalseyTypes: new SpecifiedTypes(), - specifiedNullTypes: new SpecifiedTypes(), + specifiedTruthyTypes: $specifiedExprResult->specifiedTruthyTypes, + specifiedFalseyTypes: $specifiedExprResult->specifiedFalseyTypes, + specifiedNullTypes: $specifiedExprResult->specifiedNullTypes, ); } diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index 42f2192425..766c46d9ac 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -555,3 +555,64 @@ function (array $a): void { assertType("array", $a); }; + +class CastNarrowing { + /** + * @param int $i2 + * @param mixed $m2 + */ + public function doCastNarrowing( + int $i, mixed $m, + $i2, $m2 + ): void + { + if ((bool) $i) { + assertNativeType('int|int<1, max>', $i); + } else { + assertNativeType('0', $i); + } + assertNativeType('int', $i); + + if ((bool) $i2) { + assertType('int|int<1, max>', $i2); + } else { + assertType('0', $i2); + } + assertType('int', $i2); + + if ((bool) $m) { + assertNativeType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + } else { + assertNativeType("0|0.0|''|'0'|array{}|false|null", $m); + } + assertNativeType('mixed', $m); + + if ((bool) $m2) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m2); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $m2); + } + assertType('mixed', $m2); + + if ((string) $m2) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m2); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $m2); + } + assertType('mixed', $m2); + + if ((int) $m2) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m2); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $m2); + } + assertType('mixed', $m2); + + if ((float) $m2) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m2); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $m2); + } + assertType('mixed', $m2); + } +}