From f4c0eab6e917eb542757b1bcc45fe870793049de Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 30 Jul 2026 20:14:49 +0200 Subject: [PATCH] Do not extend Expr in the isset/empty/coalesce rule-facing virtual nodes IssetExpressionNode, EmptyExpressionNode and CoalesceExpressionNode are emitted to node callbacks for their rules, and third-party collectors and rules commonly call $scope->getType() on every expression they receive - which crashed in the type-cache key printer, since these nodes have no printer method: Call to undefined method Printer::pPHPStan_Node_IssetExpressionNode() Rule-facing virtual nodes extend NodeAbstract (MatchExpressionNode, BooleanAndNode, ...) precisely so they can never reach getType(); these three now follow. callNodeCallbackWithExpression() accepts any Node - it only adjusts the scope for deep contexts before emitting. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7 --- src/Analyser/NodeScopeResolver.php | 2 +- src/Node/CoalesceExpressionNode.php | 3 +- src/Node/EmptyExpressionNode.php | 4 +- src/Node/IssetExpressionNode.php | 4 +- .../Analyser/GetTypeOnEveryNodeRuleTest.php | 48 +++++++++++++++++++ .../Analyser/data/get-type-on-every-node.php | 27 +++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Analyser/GetTypeOnEveryNodeRuleTest.php create mode 100644 tests/PHPStan/Analyser/data/get-type-on-every-node.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index eae4f4eb67f..efa23eb0517 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2912,7 +2912,7 @@ public function getAssignedVariables(Expr $expr): array */ public function callNodeCallbackWithExpression( callable $nodeCallback, - Expr $expr, + Node $expr, MutatingScope $scope, ExpressionResultStorage $storage, ExpressionContext $context, diff --git a/src/Node/CoalesceExpressionNode.php b/src/Node/CoalesceExpressionNode.php index e8653bbc426..fc5e1ff4942 100644 --- a/src/Node/CoalesceExpressionNode.php +++ b/src/Node/CoalesceExpressionNode.php @@ -4,6 +4,7 @@ use Override; use PhpParser\Node\Expr; +use PhpParser\NodeAbstract; use PHPStan\Analyser\ExpressionResult; /** @@ -14,7 +15,7 @@ * * @internal */ -final class CoalesceExpressionNode extends Expr implements VirtualNode +final class CoalesceExpressionNode extends NodeAbstract implements VirtualNode { public function __construct( diff --git a/src/Node/EmptyExpressionNode.php b/src/Node/EmptyExpressionNode.php index e6685101a76..66ae18a4d9d 100644 --- a/src/Node/EmptyExpressionNode.php +++ b/src/Node/EmptyExpressionNode.php @@ -3,8 +3,8 @@ namespace PHPStan\Node; use Override; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\Empty_; +use PhpParser\NodeAbstract; use PHPStan\Analyser\ExpressionResult; /** @@ -14,7 +14,7 @@ * * @internal */ -final class EmptyExpressionNode extends Expr implements VirtualNode +final class EmptyExpressionNode extends NodeAbstract implements VirtualNode { public function __construct(Empty_ $originalNode, private ExpressionResult $exprResult) diff --git a/src/Node/IssetExpressionNode.php b/src/Node/IssetExpressionNode.php index 6c8bc870198..640093aaf24 100644 --- a/src/Node/IssetExpressionNode.php +++ b/src/Node/IssetExpressionNode.php @@ -3,8 +3,8 @@ namespace PHPStan\Node; use Override; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\Isset_; +use PhpParser\NodeAbstract; use PHPStan\Analyser\ExpressionResult; /** @@ -14,7 +14,7 @@ * * @internal */ -final class IssetExpressionNode extends Expr implements VirtualNode +final class IssetExpressionNode extends NodeAbstract implements VirtualNode { /** diff --git a/tests/PHPStan/Analyser/GetTypeOnEveryNodeRuleTest.php b/tests/PHPStan/Analyser/GetTypeOnEveryNodeRuleTest.php new file mode 100644 index 00000000000..db38c9d2d9c --- /dev/null +++ b/tests/PHPStan/Analyser/GetTypeOnEveryNodeRuleTest.php @@ -0,0 +1,48 @@ +getType() on every + * expression they receive. Virtual nodes emitted to node callbacks must + * therefore either not be expressions at all or be resolvable and printable - + * an isset()/empty()/?? statement used to emit Expr-extending virtual nodes + * with no printer method, crashing the type-cache key printer. + * + * @extends RuleTestCase> + */ +class GetTypeOnEveryNodeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new class implements Rule { + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof Expr) { + $scope->getType($node); + } + + return []; + } + + }; + } + + public function testGetTypeOnEveryNode(): void + { + $this->analyse([__DIR__ . '/data/get-type-on-every-node.php'], []); + } + +} diff --git a/tests/PHPStan/Analyser/data/get-type-on-every-node.php b/tests/PHPStan/Analyser/data/get-type-on-every-node.php new file mode 100644 index 00000000000..1c8a2de6a84 --- /dev/null +++ b/tests/PHPStan/Analyser/data/get-type-on-every-node.php @@ -0,0 +1,27 @@ + */ + private array $data = []; + + public function sayHello(?string $name): string + { + if (isset($this->data[$name])) { + return $this->data[$name]; + } + + if (empty($this->data)) { + return 'empty'; + } + + $fallback = $name ?? 'anonymous'; + $this->data[$fallback] ??= 'hello'; + + return $this->data[$fallback]; + } + +}