From da17bb936c338539fc0676068a758d625491a410 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:29:59 +0000 Subject: [PATCH 01/14] Treat first-class callable `NullsafeMethodCall` as a `MethodCall` instead of throwing - `NodeScopeResolver::processExprNode()` handled the first-class callable syntax (`foo(...)`) for `FuncCall`, `MethodCall`, `StaticCall` and `New_`, but `NullsafeMethodCall` (`$foo?->bar(...)`) fell through to the `else` branch and threw `ShouldNotHappenException`, surfacing as an internal error. - `NullsafeMethodCall` is the only `Expr\CallLike` subclass that was missing from this branch. It is added by wrapping the call in a `MethodCallableNode` built from an equivalent `MethodCall`, since `$foo?->bar(...)` is a fatal error in PHP ("Cannot combine nullsafe operator with Closure creation") and must not crash the analyser. - Type resolution already routed correctly through `NullsafeMethodCallHandler`, so no internal error occurs anywhere the expression is typed (assignment, argument, array element, return). --- src/Analyser/NodeScopeResolver.php | 5 +++++ tests/PHPStan/Analyser/nsrt/bug-9746.php | 25 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9746.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3f63434d03..109b5cbae8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2755,6 +2755,11 @@ public function processExprNode( $newExpr = new FunctionCallableNode($expr->name, $expr); } elseif ($expr instanceof MethodCall) { $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); + } elseif ($expr instanceof Expr\NullsafeMethodCall) { + // $foo?->bar(...) is a fatal error in PHP ("Cannot combine nullsafe + // operator with Closure creation"), but it must not crash the analyser. + $methodCall = new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()); + $newExpr = new MethodCallableNode($expr->var, $expr->name, $methodCall); } elseif ($expr instanceof StaticCall) { $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-9746.php b/tests/PHPStan/Analyser/nsrt/bug-9746.php new file mode 100644 index 0000000000..043f4df6d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9746.php @@ -0,0 +1,25 @@ +method(...) is a fatal error in PHP, but PHPStan must not crash on it. + assertType('(Closure(): int)|null', $foo?->doFoo(...)); + assertType('Closure(): int', $bar?->doFoo(...)); + + $c = $foo?->doFoo(...); + assertType('(Closure(): int)|null', $c); +} From a2149b4212f20eeb97865acdce4302352de2cc3f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 16 Jun 2026 21:48:33 +0000 Subject: [PATCH 02/14] Add AnalyserIntegrationTest for first-class callable NullsafeMethodCall crash Co-Authored-By: Claude Opus 4.8 --- .../Analyser/AnalyserIntegrationTest.php | 13 +++++++++++++ tests/PHPStan/Analyser/data/bug-9746.php | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-9746.php diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index d6e9e7e38c..b633325efc 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1599,6 +1599,19 @@ public function testBug14707(): void $this->assertNoErrors($errors); } + public function testBug9746(): void + { + // first-class callable NullsafeMethodCall used to crash with an internal error + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9746.php'); + $this->assertCount(3, $errors); + $this->assertSame('Function Bug9746Integration\test() returns void but does not have any side effects.', $errors[0]->getMessage()); + $this->assertSame(15, $errors[0]->getLine()); + $this->assertSame('Call to method Bug9746Integration\Foo::doFoo() on a separate line has no effect.', $errors[1]->getMessage()); + $this->assertSame(18, $errors[1]->getLine()); + $this->assertSame('Cannot call method doFoo() on Bug9746Integration\Foo|null.', $errors[2]->getMessage()); + $this->assertSame(18, $errors[2]->getLine()); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-9746.php b/tests/PHPStan/Analyser/data/bug-9746.php new file mode 100644 index 0000000000..29c5adfaef --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9746.php @@ -0,0 +1,19 @@ +method(...) is a fatal error in PHP, but PHPStan must not crash on it. + $foo?->doFoo(...); +} From 3c8c16a479761ef506ca83767f3f4613c9829ac1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 07:10:23 +0200 Subject: [PATCH 03/14] Update bug-9746.php --- tests/PHPStan/Analyser/data/bug-9746.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/PHPStan/Analyser/data/bug-9746.php b/tests/PHPStan/Analyser/data/bug-9746.php index 29c5adfaef..153bfc5c1d 100644 --- a/tests/PHPStan/Analyser/data/bug-9746.php +++ b/tests/PHPStan/Analyser/data/bug-9746.php @@ -1,19 +1,11 @@ sayHello(...); } - -} - -function test(?Foo $foo): void -{ - // $x?->method(...) is a fatal error in PHP, but PHPStan must not crash on it. - $foo?->doFoo(...); } From 35812c74eae1c50bec0ac3c8ecc795d5f5cdc830 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 07:10:39 +0200 Subject: [PATCH 04/14] Delete tests/PHPStan/Analyser/nsrt/bug-9746.php --- tests/PHPStan/Analyser/nsrt/bug-9746.php | 25 ------------------------ 1 file changed, 25 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-9746.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-9746.php b/tests/PHPStan/Analyser/nsrt/bug-9746.php deleted file mode 100644 index 043f4df6d8..0000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-9746.php +++ /dev/null @@ -1,25 +0,0 @@ -method(...) is a fatal error in PHP, but PHPStan must not crash on it. - assertType('(Closure(): int)|null', $foo?->doFoo(...)); - assertType('Closure(): int', $bar?->doFoo(...)); - - $c = $foo?->doFoo(...); - assertType('(Closure(): int)|null', $c); -} From d31bdac350c9bbabfa18130c9be3a74969e4f9cd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:18:52 +0000 Subject: [PATCH 05/14] Report "Cannot combine nullsafe operator with Closure creation" for first-class callable NullsafeMethodCall $foo?->bar(...) is a fatal error in PHP, but PHPStan only silently treated it as a regular method-call first-class callable. Carry a nullsafe flag on MethodCallableNode and report the non-ignorable error from MethodCallableRule. Co-Authored-By: Claude Opus 4.8 --- src/Analyser/NodeScopeResolver.php | 4 +++- src/Node/MethodCallableNode.php | 10 ++++++++++ src/Rules/Methods/MethodCallableRule.php | 9 +++++++++ .../Analyser/AnalyserIntegrationTest.php | 12 +++++------- .../Rules/Methods/MethodCallableRuleTest.php | 11 +++++++++++ .../Methods/data/method-callable-nullsafe.php | 19 +++++++++++++++++++ 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 109b5cbae8..7b5e68521e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2758,8 +2758,10 @@ public function processExprNode( } elseif ($expr instanceof Expr\NullsafeMethodCall) { // $foo?->bar(...) is a fatal error in PHP ("Cannot combine nullsafe // operator with Closure creation"), but it must not crash the analyser. + // It is treated as a regular method-call first-class callable and the + // error is reported by MethodCallableRule via the nullsafe flag. $methodCall = new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()); - $newExpr = new MethodCallableNode($expr->var, $expr->name, $methodCall); + $newExpr = new MethodCallableNode($expr->var, $expr->name, $methodCall, true); } elseif ($expr instanceof StaticCall) { $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index 41aef7a962..6e1991291c 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -16,6 +16,7 @@ public function __construct( private Expr $var, private Identifier|Expr $name, private Expr\MethodCall $originalNode, + private bool $nullsafe = false, ) { parent::__construct($originalNode->getAttributes()); @@ -39,6 +40,15 @@ public function getOriginalNode(): Expr\MethodCall return $this->originalNode; } + /** + * Whether the original expression combined the nullsafe operator with the + * first-class callable syntax (`$foo?->bar(...)`), which is a fatal error in PHP. + */ + public function isNullsafe(): bool + { + return $this->nullsafe; + } + #[Override] public function getType(): string { diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 25166a91a6..8e3e7aef2a 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -37,6 +37,15 @@ public function processNode(Node $node, Scope $scope): array ]; } + if ($node->isNullsafe()) { + return [ + RuleErrorBuilder::message('Cannot combine nullsafe operator with Closure creation.') + ->nonIgnorable() + ->identifier('nullsafe.firstClassCallable') + ->build(), + ]; + } + $methodName = $node->getName(); if (!$methodName instanceof Node\Identifier) { return []; diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index b633325efc..1a092dddc5 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1603,13 +1603,11 @@ public function testBug9746(): void { // first-class callable NullsafeMethodCall used to crash with an internal error $errors = $this->runAnalyse(__DIR__ . '/data/bug-9746.php'); - $this->assertCount(3, $errors); - $this->assertSame('Function Bug9746Integration\test() returns void but does not have any side effects.', $errors[0]->getMessage()); - $this->assertSame(15, $errors[0]->getLine()); - $this->assertSame('Call to method Bug9746Integration\Foo::doFoo() on a separate line has no effect.', $errors[1]->getMessage()); - $this->assertSame(18, $errors[1]->getLine()); - $this->assertSame('Cannot call method doFoo() on Bug9746Integration\Foo|null.', $errors[2]->getMessage()); - $this->assertSame(18, $errors[2]->getLine()); + $this->assertCount(2, $errors); + $this->assertSame('Call to method Bug9746\HelloWorld::sayHello() on a separate line has no effect.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Cannot combine nullsafe operator with Closure creation.', $errors[1]->getMessage()); + $this->assertSame(9, $errors[1]->getLine()); } /** diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index 01c09e84d2..19642044fc 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -59,6 +59,17 @@ public function testBug13596(): void $this->analyse([__DIR__ . '/data/bug-13596.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testNullsafe(): void + { + $this->analyse([__DIR__ . '/data/method-callable-nullsafe.php'], [ + [ + 'Cannot combine nullsafe operator with Closure creation.', + 18, + ], + ]); + } + #[RequiresPhp('>= 8.1.0')] public function testRule(): void { diff --git a/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php b/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php new file mode 100644 index 0000000000..c46cd6fac4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php @@ -0,0 +1,19 @@ +doFoo(...); +} From acc872106f44541f342c5a98ed94ff826e457b1b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:31:46 +0000 Subject: [PATCH 06/14] Carry first-class callable nullsafe info in MethodCallableNode's original node Instead of a separate `$nullsafe` constructor flag, store the original `NullsafeMethodCall` in `MethodCallableNode::$originalNode` and derive `isNullsafe()` from an `instanceof` check. ClassStatementsGatherer converts the nullsafe node into a regular `MethodCall` when collecting it. Co-Authored-By: Claude Opus 4.8 --- src/Analyser/NodeScopeResolver.php | 5 ++--- src/Node/ClassStatementsGatherer.php | 8 +++++++- src/Node/MethodCallableNode.php | 7 +++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7b5e68521e..f7453cf8ee 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2759,9 +2759,8 @@ public function processExprNode( // $foo?->bar(...) is a fatal error in PHP ("Cannot combine nullsafe // operator with Closure creation"), but it must not crash the analyser. // It is treated as a regular method-call first-class callable and the - // error is reported by MethodCallableRule via the nullsafe flag. - $methodCall = new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()); - $newExpr = new MethodCallableNode($expr->var, $expr->name, $methodCall, true); + // error is reported by MethodCallableRule via MethodCallableNode::isNullsafe(). + $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); } elseif ($expr instanceof StaticCall) { $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index e2b278fb5b..d61d8cf6fd 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -180,7 +180,13 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) { - $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof Expr\NullsafeMethodCall) { + // $foo?->bar(...) is a fatal error in PHP, but it must not crash the + // analyser; collect it as a regular method call. + $originalNode = new MethodCall($originalNode->var, $originalNode->name, $originalNode->args, $originalNode->getAttributes()); + } + $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($originalNode, $scope); return; } if ($node instanceof MethodReturnStatementsNode) { diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index 6e1991291c..e23813a2a7 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -15,8 +15,7 @@ final class MethodCallableNode extends Expr implements VirtualNode public function __construct( private Expr $var, private Identifier|Expr $name, - private Expr\MethodCall $originalNode, - private bool $nullsafe = false, + private Expr\MethodCall|Expr\NullsafeMethodCall $originalNode, ) { parent::__construct($originalNode->getAttributes()); @@ -35,7 +34,7 @@ public function getName() return $this->name; } - public function getOriginalNode(): Expr\MethodCall + public function getOriginalNode(): Expr\MethodCall|Expr\NullsafeMethodCall { return $this->originalNode; } @@ -46,7 +45,7 @@ public function getOriginalNode(): Expr\MethodCall */ public function isNullsafe(): bool { - return $this->nullsafe; + return $this->originalNode instanceof Expr\NullsafeMethodCall; } #[Override] From b3ee78ea2228c17e87f2c21992f744dd8ca6994f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 07:42:38 +0200 Subject: [PATCH 07/14] assert based on original-node --- src/Node/MethodCallableNode.php | 9 --------- src/Rules/Methods/MethodCallableRule.php | 2 +- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 4 ++-- tests/PHPStan/Analyser/data/bug-9746.php | 4 +++- tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php | 2 +- .../Rules/Methods/data/method-callable-nullsafe.php | 4 +++- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index e23813a2a7..e73f50ae23 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -39,15 +39,6 @@ public function getOriginalNode(): Expr\MethodCall|Expr\NullsafeMethodCall return $this->originalNode; } - /** - * Whether the original expression combined the nullsafe operator with the - * first-class callable syntax (`$foo?->bar(...)`), which is a fatal error in PHP. - */ - public function isNullsafe(): bool - { - return $this->originalNode instanceof Expr\NullsafeMethodCall; - } - #[Override] public function getType(): string { diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 8e3e7aef2a..516bea879a 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -37,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - if ($node->isNullsafe()) { + if ($node->getOriginalNode() instanceof Node\Expr\NullsafeMethodCall) { return [ RuleErrorBuilder::message('Cannot combine nullsafe operator with Closure creation.') ->nonIgnorable() diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 1a092dddc5..8a59d9f8d4 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1605,9 +1605,9 @@ public function testBug9746(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-9746.php'); $this->assertCount(2, $errors); $this->assertSame('Call to method Bug9746\HelloWorld::sayHello() on a separate line has no effect.', $errors[0]->getMessage()); - $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame(11, $errors[0]->getLine()); $this->assertSame('Cannot combine nullsafe operator with Closure creation.', $errors[1]->getMessage()); - $this->assertSame(9, $errors[1]->getLine()); + $this->assertSame(11, $errors[1]->getLine()); } /** diff --git a/tests/PHPStan/Analyser/data/bug-9746.php b/tests/PHPStan/Analyser/data/bug-9746.php index 153bfc5c1d..febddd484d 100644 --- a/tests/PHPStan/Analyser/data/bug-9746.php +++ b/tests/PHPStan/Analyser/data/bug-9746.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace Bug9746; diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index 19642044fc..1e368cc926 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -65,7 +65,7 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/method-callable-nullsafe.php'], [ [ 'Cannot combine nullsafe operator with Closure creation.', - 18, + 20, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php b/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php index c46cd6fac4..2ee61cde88 100644 --- a/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php +++ b/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace MethodCallableNullsafe; From d2288b900deafea013639e3911566ec1997486d7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 06:21:31 +0000 Subject: [PATCH 08/14] Introduce NullsafeFirstClassCallableNode for $foo?->bar(...) Represent the nullsafe first-class callable in its own virtual node and report "Cannot combine nullsafe operator with Closure creation." from a dedicated NullsafeFirstClassCallableRule, instead of overloading MethodCallableNode/MethodCallableRule with a nullsafe branch. Co-Authored-By: Claude Opus 4.8 --- .../NullsafeFirstClassCallableNodeHandler.php | 73 +++++++++++++++++++ src/Analyser/NodeScopeResolver.php | 6 +- src/Dependency/DependencyResolver.php | 5 ++ src/Node/ClassStatementsGatherer.php | 8 +- src/Node/MethodCallableNode.php | 4 +- src/Node/NullsafeFirstClassCallableNode.php | 61 ++++++++++++++++ src/Node/Printer/Printer.php | 6 ++ src/Rules/Methods/MethodCallableRule.php | 9 --- .../NullsafeFirstClassCallableRule.php | 34 +++++++++ ....php => nullsafe-first-class-callable.php} | 0 10 files changed, 185 insertions(+), 21 deletions(-) create mode 100644 src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php create mode 100644 src/Node/NullsafeFirstClassCallableNode.php create mode 100644 src/Rules/Methods/NullsafeFirstClassCallableRule.php rename tests/PHPStan/Rules/Methods/data/{method-callable-nullsafe.php => nullsafe-first-class-callable.php} (100%) diff --git a/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php new file mode 100644 index 0000000000..7ae99cb58d --- /dev/null +++ b/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php @@ -0,0 +1,73 @@ + + */ +#[AutowiredService] +final class NullsafeFirstClassCallableNodeHandler implements ExprHandler +{ + + public function supports(Expr $expr): bool + { + return $expr instanceof NullsafeFirstClassCallableNode; + } + + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult + { + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $varResult->getScope(); + $hasYield = $varResult->hasYield(); + $throwPoints = $varResult->getThrowPoints(); + $impurePoints = $varResult->getImpurePoints(); + $isAlwaysTerminating = false; + if ($expr->getName() instanceof Expr) { + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $nameResult->getScope(); + $hasYield = $hasYield || $nameResult->hasYield(); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); + $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); + } + + return new ExpressionResult( + $scope, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); + } + + public function resolveType(MutatingScope $scope, Expr $expr): Type + { + // in practice the type of the first-class callable is resolved + // by NullsafeMethodCallHandler / FirstClassCallableMethodCallHandler + return new MixedType(); + } + + public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + { + return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + } + +} diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f7453cf8ee..126c80876b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -101,6 +101,7 @@ use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Node\NoopExpressionNode; +use PHPStan\Node\NullsafeFirstClassCallableNode; use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\PropertyHookReturnStatementsNode; use PHPStan\Node\PropertyHookStatementNode; @@ -2758,9 +2759,8 @@ public function processExprNode( } elseif ($expr instanceof Expr\NullsafeMethodCall) { // $foo?->bar(...) is a fatal error in PHP ("Cannot combine nullsafe // operator with Closure creation"), but it must not crash the analyser. - // It is treated as a regular method-call first-class callable and the - // error is reported by MethodCallableRule via MethodCallableNode::isNullsafe(). - $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); + // The error is reported by NullsafeFirstClassCallableRule. + $newExpr = new NullsafeFirstClassCallableNode($expr->var, $expr->name, $expr); } elseif ($expr instanceof StaticCall) { $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 87677b0f7f..58f5f81413 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -21,6 +21,7 @@ use PHPStan\Node\InPropertyHookNode; use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\MethodCallableNode; +use PHPStan\Node\NullsafeFirstClassCallableNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedParameterReflection; @@ -486,6 +487,10 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) { $dependenciesReflections[] = $dependency; } + } elseif ($node instanceof NullsafeFirstClassCallableNode) { + foreach ($this->resolveDependencies(new Node\Expr\NullsafeMethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) { + $dependenciesReflections[] = $dependency; + } } elseif ($node instanceof FunctionCallableNode) { foreach ($this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections() as $dependency) { $dependenciesReflections[] = $dependency; diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index d61d8cf6fd..e2b278fb5b 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -180,13 +180,7 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) { - $originalNode = $node->getOriginalNode(); - if ($originalNode instanceof Expr\NullsafeMethodCall) { - // $foo?->bar(...) is a fatal error in PHP, but it must not crash the - // analyser; collect it as a regular method call. - $originalNode = new MethodCall($originalNode->var, $originalNode->name, $originalNode->args, $originalNode->getAttributes()); - } - $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($originalNode, $scope); + $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); return; } if ($node instanceof MethodReturnStatementsNode) { diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php index e73f50ae23..41aef7a962 100644 --- a/src/Node/MethodCallableNode.php +++ b/src/Node/MethodCallableNode.php @@ -15,7 +15,7 @@ final class MethodCallableNode extends Expr implements VirtualNode public function __construct( private Expr $var, private Identifier|Expr $name, - private Expr\MethodCall|Expr\NullsafeMethodCall $originalNode, + private Expr\MethodCall $originalNode, ) { parent::__construct($originalNode->getAttributes()); @@ -34,7 +34,7 @@ public function getName() return $this->name; } - public function getOriginalNode(): Expr\MethodCall|Expr\NullsafeMethodCall + public function getOriginalNode(): Expr\MethodCall { return $this->originalNode; } diff --git a/src/Node/NullsafeFirstClassCallableNode.php b/src/Node/NullsafeFirstClassCallableNode.php new file mode 100644 index 0000000000..fd228ac44b --- /dev/null +++ b/src/Node/NullsafeFirstClassCallableNode.php @@ -0,0 +1,61 @@ +bar(...)` - combining the nullsafe operator with the + * first-class callable syntax. This is a fatal error in PHP ("Cannot combine + * nullsafe operator with Closure creation"), reported by NullsafeFirstClassCallableRule. + * + * @api + */ +final class NullsafeFirstClassCallableNode extends Expr implements VirtualNode +{ + + public function __construct( + private Expr $var, + private Identifier|Expr $name, + private Expr\NullsafeMethodCall $originalNode, + ) + { + parent::__construct($originalNode->getAttributes()); + } + + public function getVar(): Expr + { + return $this->var; + } + + /** + * @return Expr|Identifier + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\NullsafeMethodCall + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_NullsafeFirstClassCallableNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 7409140e8c..c0af21b77d 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -29,6 +29,7 @@ use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\IssetExpr; use PHPStan\Node\MethodCallableNode; +use PHPStan\Node\NullsafeFirstClassCallableNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -160,6 +161,11 @@ protected function pPHPStan_Node_MethodCallableNode(MethodCallableNode $expr): s return sprintf('__phpstanMethodCallable(%s)', $this->p($expr->getOriginalNode())); } + protected function pPHPStan_Node_NullsafeFirstClassCallableNode(NullsafeFirstClassCallableNode $expr): string // phpcs:ignore + { + return sprintf('__phpstanNullsafeFirstClassCallable(%s)', $this->p($expr->getOriginalNode())); + } + protected function pPHPStan_Node_StaticMethodCallableNode(StaticMethodCallableNode $expr): string // phpcs:ignore { return sprintf('__phpstanStaticMethodCallable(%s)', $this->p($expr->getOriginalNode())); diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php index 516bea879a..25166a91a6 100644 --- a/src/Rules/Methods/MethodCallableRule.php +++ b/src/Rules/Methods/MethodCallableRule.php @@ -37,15 +37,6 @@ public function processNode(Node $node, Scope $scope): array ]; } - if ($node->getOriginalNode() instanceof Node\Expr\NullsafeMethodCall) { - return [ - RuleErrorBuilder::message('Cannot combine nullsafe operator with Closure creation.') - ->nonIgnorable() - ->identifier('nullsafe.firstClassCallable') - ->build(), - ]; - } - $methodName = $node->getName(); if (!$methodName instanceof Node\Identifier) { return []; diff --git a/src/Rules/Methods/NullsafeFirstClassCallableRule.php b/src/Rules/Methods/NullsafeFirstClassCallableRule.php new file mode 100644 index 0000000000..a36897f144 --- /dev/null +++ b/src/Rules/Methods/NullsafeFirstClassCallableRule.php @@ -0,0 +1,34 @@ + + */ +#[RegisteredRule(level: 0)] +final class NullsafeFirstClassCallableRule implements Rule +{ + + public function getNodeType(): string + { + return NullsafeFirstClassCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message('Cannot combine nullsafe operator with Closure creation.') + ->nonIgnorable() + ->identifier('nullsafe.firstClassCallable') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php b/tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php similarity index 100% rename from tests/PHPStan/Rules/Methods/data/method-callable-nullsafe.php rename to tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php From daa1ab43b538a40c8cf022777b82be1bcaec67fc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 06:21:36 +0000 Subject: [PATCH 09/14] Test NullsafeFirstClassCallableRule Move the nullsafe first-class callable coverage out of MethodCallableRuleTest into a dedicated NullsafeFirstClassCallableRuleTest. Co-Authored-By: Claude Opus 4.8 --- .../Rules/Methods/MethodCallableRuleTest.php | 11 ------- .../NullsafeFirstClassCallableRuleTest.php | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index 1e368cc926..01c09e84d2 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -59,17 +59,6 @@ public function testBug13596(): void $this->analyse([__DIR__ . '/data/bug-13596.php'], []); } - #[RequiresPhp('>= 8.1.0')] - public function testNullsafe(): void - { - $this->analyse([__DIR__ . '/data/method-callable-nullsafe.php'], [ - [ - 'Cannot combine nullsafe operator with Closure creation.', - 20, - ], - ]); - } - #[RequiresPhp('>= 8.1.0')] public function testRule(): void { diff --git a/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php new file mode 100644 index 0000000000..833b9be3bb --- /dev/null +++ b/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php @@ -0,0 +1,31 @@ + + */ +class NullsafeFirstClassCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NullsafeFirstClassCallableRule(); + } + + #[RequiresPhp('>= 8.1.0')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/nullsafe-first-class-callable.php'], [ + [ + 'Cannot combine nullsafe operator with Closure creation.', + 20, + ], + ]); + } + +} From 8f7ddead20481252755a514187db5f372a6c7355 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 10:16:56 +0200 Subject: [PATCH 10/14] simplify --- .../NullsafeFirstClassCallableNodeHandler.php | 29 +++++-------------- .../NullsafeFirstClassCallableRuleTest.php | 4 +++ .../data/nullsafe-first-class-callable.php | 9 ++++++ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php index 7ae99cb58d..27ae76cb17 100644 --- a/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php @@ -34,40 +34,25 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $scope = $varResult->getScope(); - $hasYield = $varResult->hasYield(); - $throwPoints = $varResult->getThrowPoints(); - $impurePoints = $varResult->getImpurePoints(); - $isAlwaysTerminating = false; - if ($expr->getName() instanceof Expr) { - $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $scope = $nameResult->getScope(); - $hasYield = $hasYield || $nameResult->hasYield(); - $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); - $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); - } - + // NullsafeFirstClassCallableNode is a syntax error, no need to process further return new ExpressionResult( $scope, - hasYield: $hasYield, - isAlwaysTerminating: $isAlwaysTerminating, - throwPoints: $throwPoints, - impurePoints: $impurePoints, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], ); } public function resolveType(MutatingScope $scope, Expr $expr): Type { - // in practice the type of the first-class callable is resolved - // by NullsafeMethodCallHandler / FirstClassCallableMethodCallHandler + // NullsafeFirstClassCallableNode is a syntax error return new MixedType(); } public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return new SpecifiedTypes(); } } diff --git a/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php index 833b9be3bb..b028014bcb 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php @@ -25,6 +25,10 @@ public function testRule(): void 'Cannot combine nullsafe operator with Closure creation.', 20, ], + [ + 'Cannot combine nullsafe operator with Closure creation.', + 28, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php b/tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php index 2ee61cde88..2163fbdacf 100644 --- a/tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php +++ b/tests/PHPStan/Rules/Methods/data/nullsafe-first-class-callable.php @@ -19,3 +19,12 @@ function test(?Foo $foo): void // fatal error in PHP: "Cannot combine nullsafe operator with Closure creation" $foo?->doFoo(...); } + + +class HelloWorld +{ + public function sayHello(?self $self): void + { + $self?->sayHello(...); + } +} From 3e6e7d4bd98c0e5b11355f45f69a5e24e11197a1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 10:19:42 +0200 Subject: [PATCH 11/14] Delete NullsafeFirstClassCallableNodeHandler.php --- .../NullsafeFirstClassCallableNodeHandler.php | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php diff --git a/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php deleted file mode 100644 index 27ae76cb17..0000000000 --- a/src/Analyser/ExprHandler/Virtual/NullsafeFirstClassCallableNodeHandler.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ -#[AutowiredService] -final class NullsafeFirstClassCallableNodeHandler implements ExprHandler -{ - - public function supports(Expr $expr): bool - { - return $expr instanceof NullsafeFirstClassCallableNode; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // NullsafeFirstClassCallableNode is a syntax error, no need to process further - return new ExpressionResult( - $scope, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // NullsafeFirstClassCallableNode is a syntax error - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return new SpecifiedTypes(); - } - -} From 419910bc3392b73d4b6a1bc219c7934c58e99136 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 10:20:29 +0200 Subject: [PATCH 12/14] Update DependencyResolver.php --- src/Dependency/DependencyResolver.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 58f5f81413..62acd0c776 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -487,10 +487,6 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies foreach ($this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) { $dependenciesReflections[] = $dependency; } - } elseif ($node instanceof NullsafeFirstClassCallableNode) { - foreach ($this->resolveDependencies(new Node\Expr\NullsafeMethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) { - $dependenciesReflections[] = $dependency; - } } elseif ($node instanceof FunctionCallableNode) { foreach ($this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections() as $dependency) { $dependenciesReflections[] = $dependency; From ef3a400282c68981f4df93f96f6e0cae3325d2e9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 10:20:42 +0200 Subject: [PATCH 13/14] Update DependencyResolver.php --- src/Dependency/DependencyResolver.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 62acd0c776..87677b0f7f 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -21,7 +21,6 @@ use PHPStan\Node\InPropertyHookNode; use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\MethodCallableNode; -use PHPStan\Node\NullsafeFirstClassCallableNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedParameterReflection; From 6564c7be4bc19504255d0fe20e2819a0013c45c3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 17 Jun 2026 10:22:56 +0200 Subject: [PATCH 14/14] more precise name --- src/Analyser/NodeScopeResolver.php | 4 ++-- ...php => NullsafeMethodCallOnFirstClassCallableNode.php} | 2 +- src/Node/Printer/Printer.php | 4 ++-- ...php => NullsafeMethodCallOnFirstClassCallableRule.php} | 8 ++++---- ...=> NullsafeMethodCallOnFirstClassCallableRuleTest.php} | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) rename src/Node/{NullsafeFirstClassCallableNode.php => NullsafeMethodCallOnFirstClassCallableNode.php} (92%) rename src/Rules/Methods/{NullsafeFirstClassCallableRule.php => NullsafeMethodCallOnFirstClassCallableRule.php} (68%) rename tests/PHPStan/Rules/Methods/{NullsafeFirstClassCallableRuleTest.php => NullsafeMethodCallOnFirstClassCallableRuleTest.php} (72%) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 126c80876b..6e9ee37c63 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -101,7 +101,7 @@ use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Node\NoopExpressionNode; -use PHPStan\Node\NullsafeFirstClassCallableNode; +use PHPStan\Node\NullsafeMethodCallOnFirstClassCallableNode; use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\PropertyHookReturnStatementsNode; use PHPStan\Node\PropertyHookStatementNode; @@ -2760,7 +2760,7 @@ public function processExprNode( // $foo?->bar(...) is a fatal error in PHP ("Cannot combine nullsafe // operator with Closure creation"), but it must not crash the analyser. // The error is reported by NullsafeFirstClassCallableRule. - $newExpr = new NullsafeFirstClassCallableNode($expr->var, $expr->name, $expr); + $newExpr = new NullsafeMethodCallOnFirstClassCallableNode($expr->var, $expr->name, $expr); } elseif ($expr instanceof StaticCall) { $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { diff --git a/src/Node/NullsafeFirstClassCallableNode.php b/src/Node/NullsafeMethodCallOnFirstClassCallableNode.php similarity index 92% rename from src/Node/NullsafeFirstClassCallableNode.php rename to src/Node/NullsafeMethodCallOnFirstClassCallableNode.php index fd228ac44b..4b148aee6a 100644 --- a/src/Node/NullsafeFirstClassCallableNode.php +++ b/src/Node/NullsafeMethodCallOnFirstClassCallableNode.php @@ -13,7 +13,7 @@ * * @api */ -final class NullsafeFirstClassCallableNode extends Expr implements VirtualNode +final class NullsafeMethodCallOnFirstClassCallableNode extends Expr implements VirtualNode { public function __construct( diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index c0af21b77d..7c6c8dbcb8 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -29,7 +29,7 @@ use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\IssetExpr; use PHPStan\Node\MethodCallableNode; -use PHPStan\Node\NullsafeFirstClassCallableNode; +use PHPStan\Node\NullsafeMethodCallOnFirstClassCallableNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -161,7 +161,7 @@ protected function pPHPStan_Node_MethodCallableNode(MethodCallableNode $expr): s return sprintf('__phpstanMethodCallable(%s)', $this->p($expr->getOriginalNode())); } - protected function pPHPStan_Node_NullsafeFirstClassCallableNode(NullsafeFirstClassCallableNode $expr): string // phpcs:ignore + protected function pPHPStan_Node_NullsafeFirstClassCallableNode(NullsafeMethodCallOnFirstClassCallableNode $expr): string // phpcs:ignore { return sprintf('__phpstanNullsafeFirstClassCallable(%s)', $this->p($expr->getOriginalNode())); } diff --git a/src/Rules/Methods/NullsafeFirstClassCallableRule.php b/src/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRule.php similarity index 68% rename from src/Rules/Methods/NullsafeFirstClassCallableRule.php rename to src/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRule.php index a36897f144..e55f7bfd8f 100644 --- a/src/Rules/Methods/NullsafeFirstClassCallableRule.php +++ b/src/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRule.php @@ -5,20 +5,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\NullsafeFirstClassCallableNode; +use PHPStan\Node\NullsafeMethodCallOnFirstClassCallableNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 0)] -final class NullsafeFirstClassCallableRule implements Rule +final class NullsafeMethodCallOnFirstClassCallableRule implements Rule { public function getNodeType(): string { - return NullsafeFirstClassCallableNode::class; + return NullsafeMethodCallOnFirstClassCallableNode::class; } public function processNode(Node $node, Scope $scope): array diff --git a/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRuleTest.php similarity index 72% rename from tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php rename to tests/PHPStan/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRuleTest.php index b028014bcb..1d96d5bf6c 100644 --- a/tests/PHPStan/Rules/Methods/NullsafeFirstClassCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/NullsafeMethodCallOnFirstClassCallableRuleTest.php @@ -7,14 +7,14 @@ use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class NullsafeFirstClassCallableRuleTest extends RuleTestCase +class NullsafeMethodCallOnFirstClassCallableRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new NullsafeFirstClassCallableRule(); + return new NullsafeMethodCallOnFirstClassCallableRule(); } #[RequiresPhp('>= 8.1.0')]