diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/with_trailing_comma.php.inc b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/with_trailing_comma.php.inc new file mode 100644 index 00000000000..dad4dc64517 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/with_trailing_comma.php.inc @@ -0,0 +1,53 @@ +map( + $array, + fn($item) => var_dump( + $item, + ) + ); + } + + private function map(array $items, callable $callback): void { + foreach($items as $i) { + $callback($i); + } + } +} + +?> +----- +map( + $array, + var_dump( + ... + ) + ); + } + + private function map(array $items, callable $callback): void { + foreach($items as $i) { + $callback($i); + } + } +} + +?> diff --git a/src/PhpParser/Printer/BetterStandardPrinter.php b/src/PhpParser/Printer/BetterStandardPrinter.php index 22e95332e11..2529326e8f9 100644 --- a/src/PhpParser/Printer/BetterStandardPrinter.php +++ b/src/PhpParser/Printer/BetterStandardPrinter.php @@ -31,6 +31,7 @@ use PhpParser\Node\Stmt\InlineHTML; use PhpParser\Node\Stmt\Nop; use PhpParser\PrettyPrinter\Standard; +use PhpParser\Token; use PHPStan\Node\AnonymousClassNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use Rector\Configuration\Option; @@ -39,6 +40,7 @@ use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; use Rector\Util\NewLineSplitter; +use Rector\Util\Reflection\PrivatesAccessor; use Rector\Util\StringUtils; /** @@ -62,7 +64,8 @@ final class BetterStandardPrinter extends Standard private const SPACED_NEW_START_REGEX = '#^new\s+#'; public function __construct( - private readonly ExprAnalyzer $exprAnalyzer + private readonly ExprAnalyzer $exprAnalyzer, + private readonly PrivatesAccessor $privatesAccessor, ) { parent::__construct(); } @@ -165,6 +168,10 @@ protected function p( $content = 'new ' . $content; } + if ($node instanceof CallLike) { + $this->cleanVariadicPlaceHolderTrailingComma($node); + } + return $node->getAttribute(AttributeKey::WRAPPED_IN_PARENTHESES) === true ? ('(' . $content . ')') : $content; @@ -453,6 +460,45 @@ protected function pExpr_Instanceof(Instanceof_ $instanceof, int $precedence, in return parent::pExpr_Instanceof($instanceof, $precedence, $lhsPrecedence); } + private function cleanVariadicPlaceHolderTrailingComma(CallLike $callLike): void + { + $originalNode = $callLike->getAttribute(AttributeKey::ORIGINAL_NODE); + if (! $originalNode instanceof CallLike) { + return; + } + + if ($originalNode->isFirstClassCallable()) { + return; + } + + if (! $callLike->isFirstClassCallable()) { + return; + } + + if (! $this->origTokens instanceof TokenStream) { + return; + } + + /** @var Token[] $tokens */ + $tokens = $this->privatesAccessor->getPrivateProperty($this->origTokens, 'tokens'); + + $iteration = 1; + while (isset($tokens[$callLike->getEndTokenPos() - $iteration])) { + $text = trim((string) $tokens[$callLike->getEndTokenPos() - $iteration]->text); + + if (in_array($text, [')', ''], true)) { + ++$iteration; + continue; + } + + if ($text === ',') { + $tokens[$callLike->getEndTokenPos() - $iteration]->text = ''; + } + + break; + } + } + private function wrapBinaryOp(Node $node): void { if ($this->exprAnalyzer->isExprWithExprPropertyWrappable($node)) {