diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index b7eb0e843e5..a9e7ecc0a3d 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -18,6 +18,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\ExprUsedAsStringNode; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantIntegerType; @@ -99,6 +100,8 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExprUsedAsStringNode($expr->expr, $expr), $scope, $storage); } return new ExpressionResult( diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index b4e119e01f5..e7c21ba210d 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\ExprUsedAsStringNode; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; use function array_merge; @@ -46,6 +47,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExprUsedAsStringNode($expr->expr, $expr), $scope, $storage); + $scope = $exprResult->getScope(); return new ExpressionResult( diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index bb18cff3e09..0805f9700ae 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\ExprUsedAsStringNode; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; @@ -61,6 +62,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $partResult->getScope(); } + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExprUsedAsStringNode($expr, $expr), $scope, $storage); + return new ExpressionResult( $scope, hasYield: $hasYield, diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 18ab04d8d29..7b2e47a81fd 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\ExprUsedAsStringNode; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Type; use function array_merge; @@ -51,6 +52,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExprUsedAsStringNode($expr->expr, $expr), $scope, $storage); + $scope = $exprResult->getScope(); return new ExpressionResult( diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d013b03359a..ddb7479e5c8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -85,6 +85,7 @@ use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\Expr\UnsetOffsetExpr; +use PHPStan\Node\ExprUsedAsStringNode; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionReturnStatementsNode; @@ -996,6 +997,7 @@ public function processStmtNode( $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + $this->callNodeCallback($nodeCallback, new ExprUsedAsStringNode($echoExpr, $stmt), $scope, $storage); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); @@ -2402,6 +2404,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch $impurePoints = [ new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true), ]; + $this->callNodeCallback($nodeCallback, new ExprUsedAsStringNode(new TypeExpr(new ConstantStringType($stmt->value)), $stmt), $scope, $storage); } elseif ($stmt instanceof Node\Stmt\Block) { $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context); if ($this->polluteScopeWithBlock) { diff --git a/src/Node/ExprUsedAsStringNode.php b/src/Node/ExprUsedAsStringNode.php new file mode 100644 index 00000000000..0b463bbc7c9 --- /dev/null +++ b/src/Node/ExprUsedAsStringNode.php @@ -0,0 +1,46 @@ +getAttributes()); + } + + public function getExpression(): Expr + { + return $this->expression; + } + + public function getOriginalNode(): Expr|Node\Stmt + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_ExprUsedAsStringNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringRuleTest.php b/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringRuleTest.php new file mode 100644 index 00000000000..bffaec91176 --- /dev/null +++ b/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringRuleTest.php @@ -0,0 +1,71 @@ + + */ +class ExprUsedAsStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ExprUsedAsStringTestRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/expr-used-as-string.php'], [ + [ + 'Expression used as string: PhpParser\Node\Scalar\String_ in PhpParser\Node\Stmt\Echo_', + 8, + ], + [ + 'Expression used as string: PhpParser\Node\Scalar\String_ in PhpParser\Node\Stmt\Echo_', + 9, + ], + [ + 'Expression used as string: PhpParser\Node\Scalar\String_ in PhpParser\Node\Stmt\Echo_', + 9, + ], + [ + 'Expression used as string: PhpParser\Node\Scalar\String_ in PhpParser\Node\Expr\Print_', + 13, + ], + [ + 'Expression used as string: PhpParser\Node\Expr\Variable in PhpParser\Node\Expr\Cast\String_', + 17, + ], + [ + 'Expression used as string: PhpParser\Node\Scalar\InterpolatedString in PhpParser\Node\Scalar\InterpolatedString', + 26, + ], + [ + 'Expression used as string: PhpParser\Node\Expr\Variable in PhpParser\Node\Expr\AssignOp\Concat', + 30, + ], + [ + 'Expression used as string: PhpParser\Node\Expr\BinaryOp\Concat in PhpParser\Node\Stmt\Echo_', + 36, + ], + [ + 'Expression used as string: PhpParser\Node\Scalar\InterpolatedString in PhpParser\Node\Scalar\InterpolatedString', + 41, + ], + ]); + } + + public function testInlineHtml(): void + { + $this->analyse([__DIR__ . '/data/inline-html.php'], [ + [ + 'Expression used as string: PHPStan\Node\Expr\TypeExpr in PhpParser\Node\Stmt\InlineHTML', + 7, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringTestRule.php b/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringTestRule.php new file mode 100644 index 00000000000..37073e066a3 --- /dev/null +++ b/tests/PHPStan/Rules/ExprUsedAsString/ExprUsedAsStringTestRule.php @@ -0,0 +1,35 @@ + + */ +class ExprUsedAsStringTestRule implements Rule +{ + + public function getNodeType(): string + { + return ExprUsedAsStringNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $expr = $node->getExpression(); + $originalNode = $node->getOriginalNode(); + + return [ + RuleErrorBuilder::message('Expression used as string: ' . get_class($expr) . ' in ' . get_class($originalNode)) + ->identifier('tests.exprUsedAsString') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/ExprUsedAsString/data/expr-used-as-string.php b/tests/PHPStan/Rules/ExprUsedAsString/data/expr-used-as-string.php new file mode 100644 index 00000000000..797df65bdc3 --- /dev/null +++ b/tests/PHPStan/Rules/ExprUsedAsString/data/expr-used-as-string.php @@ -0,0 +1,44 @@ += 8.0 + +namespace ExprUsedAsString; + +class Foo { + + public function doEcho(): void { + echo 'hello'; + echo 'hello', ' world'; + } + + public function doPrint(): void { + print 'hello'; + } + + public function doCast(int $i): void { + (string) $i; + } + + public function doConcat(string $a, string $b): void { + $a . $b; + 'a' . $b . 'c'; + } + + public function doInterpolatedString(string $name): void { + "Hello $name!"; + } + + public function doConcatAssign(string $a, string $b): void { + $a .= $b; + } + +} + +function doEchoConcat(string $s): void { + echo ''; +} + +function doHeredoc(): void { + $nonce = '123'; + $html = << + EOS; +} diff --git a/tests/PHPStan/Rules/ExprUsedAsString/data/inline-html.php b/tests/PHPStan/Rules/ExprUsedAsString/data/inline-html.php new file mode 100644 index 00000000000..dfeb8f3f992 --- /dev/null +++ b/tests/PHPStan/Rules/ExprUsedAsString/data/inline-html.php @@ -0,0 +1,9 @@ + + +