From 52980d0f81e81f3f8e8b1f07ecc3b3ce4d594868 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 1 Dec 2025 08:08:48 +0100 Subject: [PATCH 1/3] Implement InterpolatedStringHandler --- .../ExprHandler/InterpolatedStringHandler.php | 90 +++++++++++++++++++ .../PHPStan/Analyser/Generator/data/gnsr.php | 8 ++ 2 files changed, 98 insertions(+) create mode 100644 src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php diff --git a/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php new file mode 100644 index 0000000000..01a29e84eb --- /dev/null +++ b/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php @@ -0,0 +1,90 @@ + + */ +#[AutowiredService] +final class InterpolatedStringHandler implements ExprHandler +{ + + public function __construct(private InitializerExprTypeResolver $initializerExprTypeResolver) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof InterpolatedString; + } + + public function analyseExpr( + Stmt $stmt, + Expr $expr, + GeneratorScope $scope, + ExpressionContext $context, + ?callable $alternativeNodeCallback, + ): Generator + { + yield from []; + + $resultType = null; + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + foreach ($expr->parts as $part) { + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $result = yield new ExprAnalysisRequest($stmt, $part, $scope, $context->enterDeep(), new NoopNodeCallback()); + $partType = $result->type->toString(); + + $hasYield = $hasYield || $result->hasYield; + $throwPoints = array_merge($throwPoints, $result->throwPoints); + $impurePoints = array_merge($impurePoints, $result->impurePoints); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating; + $scope = $result->scope; + } + + if ($resultType === null) { + $resultType = $partType; + continue; + } + + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + } + + $type = $resultType ?? new ConstantStringType(''); + return new ExprAnalysisResult( + $type, + $type, + $scope, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + specifiedTruthyTypes: new SpecifiedTypes(), + specifiedFalseyTypes: new SpecifiedTypes(), + specifiedNullTypes: new SpecifiedTypes(), + ); + } + +} diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index a142ab7bed..f0421f8eba 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -247,6 +247,14 @@ function doCast() { assertType("'1'", (string) $a); } + function doInterpolatedString() { + $a = '1'; + + assertType("'1'", "$a"); + assertNativeType("'1'", "$a"); + } + + } function (): void { From 5ebaa40e4a32d6e6cbaac39d5545acbd6d48bb9d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 1 Dec 2025 09:55:11 +0100 Subject: [PATCH 2/3] fix --- .../ExprHandler/InterpolatedStringHandler.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php index 01a29e84eb..0646f66bf2 100644 --- a/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/Generator/ExprHandler/InterpolatedStringHandler.php @@ -12,10 +12,10 @@ 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; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; use function array_merge; @@ -46,6 +46,8 @@ public function analyseExpr( yield from []; $resultType = null; + $resultNativeType = null; + $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -53,9 +55,11 @@ public function analyseExpr( foreach ($expr->parts as $part) { if ($part instanceof InterpolatedStringPart) { $partType = new ConstantStringType($part->value); + $partNativeType = $partType; } else { - $result = yield new ExprAnalysisRequest($stmt, $part, $scope, $context->enterDeep(), new NoopNodeCallback()); + $result = yield new ExprAnalysisRequest($stmt, $part, $scope, $context->enterDeep(), $alternativeNodeCallback); $partType = $result->type->toString(); + $partNativeType = $result->nativeType->toString(); $hasYield = $hasYield || $result->hasYield; $throwPoints = array_merge($throwPoints, $result->throwPoints); @@ -66,16 +70,21 @@ public function analyseExpr( if ($resultType === null) { $resultType = $partType; + $resultNativeType = $partNativeType; continue; } + if ($resultNativeType === null) { + throw new ShouldNotHappenException(); + } + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + $resultNativeType = $this->initializerExprTypeResolver->resolveConcatType($resultNativeType, $partNativeType); } - $type = $resultType ?? new ConstantStringType(''); return new ExprAnalysisResult( - $type, - $type, + $resultType ?? new ConstantStringType(''), + $resultNativeType ?? new ConstantStringType(''), $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, From 75827ee0528c08f2cba66017cf0d4d0566aa880a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 1 Dec 2025 09:56:41 +0100 Subject: [PATCH 3/3] Update gnsr.php --- tests/PHPStan/Analyser/Generator/data/gnsr.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/Generator/data/gnsr.php b/tests/PHPStan/Analyser/Generator/data/gnsr.php index f0421f8eba..0cb34eeada 100644 --- a/tests/PHPStan/Analyser/Generator/data/gnsr.php +++ b/tests/PHPStan/Analyser/Generator/data/gnsr.php @@ -247,11 +247,16 @@ function doCast() { assertType("'1'", (string) $a); } - function doInterpolatedString() { + /** + * @param '1' $b + */ + function doInterpolatedString(string $b) { $a = '1'; assertType("'1'", "$a"); assertNativeType("'1'", "$a"); + assertType("'1'", "$b"); + assertNativeType("string", "$b"); }