Skip to content

Commit f935366

Browse files
ondrejmirtesclaude
andcommitted
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
1 parent 410436a commit f935366

6 files changed

Lines changed: 82 additions & 6 deletions

File tree

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2912,7 +2912,7 @@ public function getAssignedVariables(Expr $expr): array
29122912
*/
29132913
public function callNodeCallbackWithExpression(
29142914
callable $nodeCallback,
2915-
Expr $expr,
2915+
Node $expr,
29162916
MutatingScope $scope,
29172917
ExpressionResultStorage $storage,
29182918
ExpressionContext $context,

‎src/Node/CoalesceExpressionNode.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Override;
66
use PhpParser\Node\Expr;
7+
use PhpParser\NodeAbstract;
78
use PHPStan\Analyser\ExpressionResult;
89

910
/**
@@ -14,7 +15,7 @@
1415
*
1516
* @internal
1617
*/
17-
final class CoalesceExpressionNode extends Expr implements VirtualNode
18+
final class CoalesceExpressionNode extends NodeAbstract implements VirtualNode
1819
{
1920

2021
public function __construct(

‎src/Node/EmptyExpressionNode.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace PHPStan\Node;
44

55
use Override;
6-
use PhpParser\Node\Expr;
76
use PhpParser\Node\Expr\Empty_;
7+
use PhpParser\NodeAbstract;
88
use PHPStan\Analyser\ExpressionResult;
99

1010
/**
@@ -14,7 +14,7 @@
1414
*
1515
* @internal
1616
*/
17-
final class EmptyExpressionNode extends Expr implements VirtualNode
17+
final class EmptyExpressionNode extends NodeAbstract implements VirtualNode
1818
{
1919

2020
public function __construct(Empty_ $originalNode, private ExpressionResult $exprResult)

‎src/Node/IssetExpressionNode.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
namespace PHPStan\Node;
44

55
use Override;
6-
use PhpParser\Node\Expr;
76
use PhpParser\Node\Expr\Isset_;
7+
use PhpParser\NodeAbstract;
88
use PHPStan\Analyser\ExpressionResult;
99

1010
/**
@@ -14,7 +14,7 @@
1414
*
1515
* @internal
1616
*/
17-
final class IssetExpressionNode extends Expr implements VirtualNode
17+
final class IssetExpressionNode extends NodeAbstract implements VirtualNode
1818
{
1919

2020
/**
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* Third-party collectors and rules commonly call $scope->getType() on every
12+
* expression they receive. Virtual nodes emitted to node callbacks must
13+
* therefore either not be expressions at all or be resolvable and printable -
14+
* an isset()/empty()/?? statement used to emit Expr-extending virtual nodes
15+
* with no printer method, crashing the type-cache key printer.
16+
*
17+
* @extends RuleTestCase<Rule<Node>>
18+
*/
19+
class GetTypeOnEveryNodeRuleTest extends RuleTestCase
20+
{
21+
22+
protected function getRule(): Rule
23+
{
24+
return new class implements Rule {
25+
26+
public function getNodeType(): string
27+
{
28+
return Node::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if ($node instanceof Expr) {
34+
$scope->getType($node);
35+
}
36+
37+
return [];
38+
}
39+
40+
};
41+
}
42+
43+
public function testGetTypeOnEveryNode(): void
44+
{
45+
$this->analyse([__DIR__ . '/data/get-type-on-every-node.php'], []);
46+
}
47+
48+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GetTypeOnEveryNode;
4+
5+
class HelloWorld
6+
{
7+
8+
/** @var array<string, string> */
9+
private array $data = [];
10+
11+
public function sayHello(?string $name): string
12+
{
13+
if (isset($this->data[$name])) {
14+
return $this->data[$name];
15+
}
16+
17+
if (empty($this->data)) {
18+
return 'empty';
19+
}
20+
21+
$fallback = $name ?? 'anonymous';
22+
$this->data[$fallback] ??= 'hello';
23+
24+
return $this->data[$fallback];
25+
}
26+
27+
}

0 commit comments

Comments
 (0)