Repository: PHP-CS-Fixer/PHP-CS-Fixer (PHP)

Issue:
Trailing commas for match cases
## Feature request

Right now TrailingCommaInMultilineFixer can fix missing trailing commas in arrays, arguments and parameters. It should be appliable to match cases in PHP >= 8.0.

For example the following code:
```php
$message = match ($statusCode) {
    200, 300 => null,
    400 => 'not found',
    500 => 'server error',
    default => 'unknown status code'
};
```

should be fixed into:
```php
$message = match ($statusCode) {
    200, 300 => null,
    400 => 'not found',
    500 => 'server error',
    default => 'unknown status code',
};
```


Discussion:
Thanks for this suggestion,. if you can please try out https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/6376 and let me know what you think :+1: 
@keradus, when can I expect release with these changes?



Patch:
diff --git a/doc/list.rst b/doc/list.rst
index 1a40fae37b4..ee3a5154d89 100644
--- a/doc/list.rst
+++ b/doc/list.rst
@@ -2854,7 +2854,7 @@ List of Available Rules
    `Source PhpCsFixer\\Fixer\\Operator\\TernaryToNullCoalescingFixer <./../src/Fixer/Operator/TernaryToNullCoalescingFixer.php>`_
 -  `trailing_comma_in_multiline <./rules/control_structure/trailing_comma_in_multiline.rst>`_
 
-   Multi-line arrays, arguments list and parameters list must have a trailing comma.
+   Multi-line arrays, arguments list, parameters list and ``match`` expressions must have a trailing comma.
 
    Configuration options:
 
@@ -2863,8 +2863,8 @@ List of Available Rules
      | Allowed types: ``bool``
      | Default value: ``false``
    - | ``elements``
-     | Where to fix multiline trailing comma (PHP >= 7.3 required for `arguments`, PHP >= 8.0 for `parameters`).
-     | Allowed values: a subset of ``['arrays', 'arguments', 'parameters']``
+     | Where to fix multiline trailing comma (PHP >= 8.0 for `parameters` and `match`).
+     | Allowed values: a subset of ``['arrays', 'arguments', 'parameters', 'match']``
      | Default value: ``['arrays']``
 
 
diff --git a/doc/rules/control_structure/trailing_comma_in_multiline.rst b/doc/rules/control_structure/trailing_comma_in_multiline.rst
index ece2a1ab42f..7909efe41f3 100644
--- a/doc/rules/control_structure/trailing_comma_in_multiline.rst
+++ b/doc/rules/control_structure/trailing_comma_in_multiline.rst
@@ -2,8 +2,8 @@
 Rule ``trailing_comma_in_multiline``
 ====================================
 
-Multi-line arrays, arguments list and parameters list must have a trailing
-comma.
+Multi-line arrays, arguments list, parameters list and ``match`` expressions
+must have a trailing comma.
 
 Configuration
 -------------
@@ -20,10 +20,10 @@ Default value: ``false``
 ``elements``
 ~~~~~~~~~~~~
 
-Where to fix multiline trailing comma (PHP >= 7.3 required for ``arguments``,
-PHP >= 8.0 for ``parameters``).
+Where to fix multiline trailing comma (PHP >= 8.0 for ``parameters`` and
+``match``).
 
-Allowed values: a subset of ``['arrays', 'arguments', 'parameters']``
+Allowed values: a subset of ``['arrays', 'arguments', 'parameters', 'match']``
 
 Default value: ``['arrays']``
 
diff --git a/doc/rules/index.rst b/doc/rules/index.rst
index cddd347f8b9..4f15c956ab6 100644
--- a/doc/rules/index.rst
+++ b/doc/rules/index.rst
@@ -286,7 +286,7 @@ Control Structure
   Switch case must not be ended with ``continue`` but with ``break``.
 - `trailing_comma_in_multiline <./control_structure/trailing_comma_in_multiline.rst>`_
 
-  Multi-line arrays, arguments list and parameters list must have a trailing comma.
+  Multi-line arrays, arguments list, parameters list and ``match`` expressions must have a trailing comma.
 - `yoda_style <./control_structure/yoda_style.rst>`_
 
   Write conditions in Yoda style (``true``), non-Yoda style (``['equal' => false, 'identical' => false, 'less_and_greater' => false]``) or ignore those conditions (``null``) based on configuration.
diff --git a/src/Fixer/ControlStructure/TrailingCommaInMultilineFixer.php b/src/Fixer/ControlStructure/TrailingCommaInMultilineFixer.php
index 51099dff0a0..dc8c3060548 100644
--- a/src/Fixer/ControlStructure/TrailingCommaInMultilineFixer.php
+++ b/src/Fixer/ControlStructure/TrailingCommaInMultilineFixer.php
@@ -54,13 +54,15 @@ final class TrailingCommaInMultilineFixer extends AbstractFixer implements Confi
      */
     public const ELEMENTS_PARAMETERS = 'parameters';
 
+    private const MATCH_EXPRESSIONS = 'match';
+
     /**
      * {@inheritdoc}
      */
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            'Multi-line arrays, arguments list and parameters list must have a trailing comma.',
+            'Multi-line arrays, arguments list, parameters list and `match` expressions must have a trailing comma.',
             [
                 new CodeSample("<?php\narray(\n    1,\n    2\n);\n"),
                 new VersionSpecificCodeSample(
@@ -115,13 +117,17 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
                     return $value;
                 })
                 ->getOption(),
-            (new FixerOptionBuilder('elements', sprintf('Where to fix multiline trailing comma (PHP >= 7.3 required for `%s`, PHP >= 8.0 for `%s`).', self::ELEMENTS_ARGUMENTS, self::ELEMENTS_PARAMETERS)))
+            (new FixerOptionBuilder('elements', sprintf('Where to fix multiline trailing comma (PHP >= 8.0 for `%s` and `%s`).', self::ELEMENTS_PARAMETERS, self::MATCH_EXPRESSIONS))) // @TODO: remove text when PHP 8.0+ is required
                 ->setAllowedTypes(['array'])
-                ->setAllowedValues([new AllowedValueSubset([self::ELEMENTS_ARRAYS, self::ELEMENTS_ARGUMENTS, self::ELEMENTS_PARAMETERS])])
+                ->setAllowedValues([new AllowedValueSubset([self::ELEMENTS_ARRAYS, self::ELEMENTS_ARGUMENTS, self::ELEMENTS_PARAMETERS, self::MATCH_EXPRESSIONS])])
                 ->setDefault([self::ELEMENTS_ARRAYS])
                 ->setNormalizer(static function (Options $options, $value) {
-                    if (\PHP_VERSION_ID < 80000 && \in_array(self::ELEMENTS_PARAMETERS, $value, true)) {
-                        throw new InvalidOptionsForEnvException(sprintf('"%s" option can only be enabled with PHP 8.0+.', self::ELEMENTS_PARAMETERS));
+                    if (\PHP_VERSION_ID < 80000) { // @TODO: drop condition when PHP 8.0+ is required
+                        foreach ([self::ELEMENTS_PARAMETERS, self::MATCH_EXPRESSIONS] as $option) {
+                            if (\in_array($option, $value, true)) {
+                                throw new InvalidOptionsForEnvException(sprintf('"%s" option can only be enabled with PHP 8.0+.', $option));
+                            }
+                        }
                     }
 
                     return $value;
@@ -137,7 +143,8 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
     {
         $fixArrays = \in_array(self::ELEMENTS_ARRAYS, $this->configuration['elements'], true);
         $fixArguments = \in_array(self::ELEMENTS_ARGUMENTS, $this->configuration['elements'], true);
-        $fixParameters = \in_array(self::ELEMENTS_PARAMETERS, $this->configuration['elements'], true);
+        $fixParameters = \PHP_VERSION_ID >= 80000 && \in_array(self::ELEMENTS_PARAMETERS, $this->configuration['elements'], true); // @TODO: drop condition when PHP 8.0+ is required
+        $fixMatch = \PHP_VERSION_ID >= 80000 && \in_array(self::MATCH_EXPRESSIONS, $this->configuration['elements'], true); // @TODO: drop condition when PHP 8.0+ is required
 
         for ($index = $tokens->count() - 1; $index >= 0; --$index) {
             $prevIndex = $tokens->getPrevMeaningfulToken($index);
@@ -178,6 +185,10 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
             ) {
                 $this->fixBlock($tokens, $index);
             }
+
+            if ($fixMatch && $tokens[$prevIndex]->isGivenKind(T_MATCH)) {
+                $this->fixMatch($tokens, $index);
+            }
         }
     }
 
@@ -209,4 +220,34 @@ private function fixBlock(Tokens $tokens, int $startIndex): void
             }
         }
     }
+
+    private function fixMatch(Tokens $tokens, int $index): void
+    {
+        $index = $tokens->getNextTokenOfKind($index, ['{']);
+        $closeIndex = $index;
+        $isMultiline = false;
+        $depth = 1;
+
+        do {
+            ++$closeIndex;
+
+            if ($tokens[$closeIndex]->equals('{')) {
+                ++$depth;
+            } elseif ($tokens[$closeIndex]->equals('}')) {
+                --$depth;
+            } elseif (!$isMultiline && str_contains($tokens[$closeIndex]->getContent(), "\n")) {
+                $isMultiline = true;
+            }
+        } while ($depth > 0);
+
+        if (!$isMultiline) {
+            return;
+        }
+
+        $previousIndex = $tokens->getPrevMeaningfulToken($closeIndex);
+
+        if (!$tokens[$previousIndex]->equals(',')) {
+            $tokens->insertAt($previousIndex + 1, new Token(','));
+        }
+    }
 }


Test patch:
diff --git a/tests/Fixer/ControlStructure/TrailingCommaInMultilineFixerTest.php b/tests/Fixer/ControlStructure/TrailingCommaInMultilineFixerTest.php
index d07e08a392f..2a5924ce7c6 100644
--- a/tests/Fixer/ControlStructure/TrailingCommaInMultilineFixerTest.php
+++ b/tests/Fixer/ControlStructure/TrailingCommaInMultilineFixerTest.php
@@ -44,7 +44,7 @@ public function testInvalidConfiguration($exceptionMessega, $configuration): voi
         $this->fixer->configure($configuration);
     }
 
-    public static function provideInvalidConfigurationCases(): \Generator
+    public static function provideInvalidConfigurationCases(): iterable
     {
         yield [
             '[trailing_comma_in_multiline] Invalid configuration for env: "parameters" option can only be enabled with PHP 8.0+.',
@@ -551,55 +551,93 @@ public function testFix80(string $expected, ?string $input = null, array $config
         $this->doTest($expected, $input);
     }
 
-    public static function provideFix80Cases(): array
+    public static function provideFix80Cases(): iterable
     {
-        return [
-            [
-                '<?php function foo($x, $y) {}',
-                null,
-                ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
-            ],
-            [
-                '<?php function foo(
-                        $x,
-                        $y
-                    ) {}',
-                null, // do not fix if not configured
-                ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_ARRAYS, TrailingCommaInMultilineFixer::ELEMENTS_ARGUMENTS]],
-            ],
-            [
-                '<?php function foo(
-                        $x,
-                        $y,
-                    ) {}',
-                '<?php function foo(
-                        $x,
-                        $y
-                    ) {}',
-                ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
-            ],
-            [
-                '<?php $x = function(
-                        $x,
-                        $y,
-                    ) {};',
-                '<?php $x = function(
-                        $x,
-                        $y
-                    ) {};',
-                ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
-            ],
-            [
-                '<?php $x = fn(
-                        $x,
-                        $y,
-                    ) => $x + $y;',
-                '<?php $x = fn(
-                        $x,
-                        $y
-                    ) => $x + $y;',
-                ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
-            ],
+        yield [
+            '<?php function foo($x, $y) {}',
+            null,
+            ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
+        ];
+
+        yield [
+            '<?php function foo(
+                    $x,
+                    $y
+                ) {}',
+            null, // do not fix if not configured
+            ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_ARRAYS, TrailingCommaInMultilineFixer::ELEMENTS_ARGUMENTS]],
+        ];
+
+        yield [
+            '<?php function foo(
+                    $x,
+                    $y,
+                ) {}',
+            '<?php function foo(
+                    $x,
+                    $y
+                ) {}',
+            ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
+        ];
+
+        yield [
+            '<?php $x = function(
+                    $x,
+                    $y,
+                ) {};',
+            '<?php $x = function(
+                    $x,
+                    $y
+                ) {};',
+            ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
+        ];
+
+        yield [
+            '<?php $x = fn(
+                    $x,
+                    $y,
+                ) => $x + $y;',
+            '<?php $x = fn(
+                    $x,
+                    $y
+                ) => $x + $y;',
+            ['elements' => [TrailingCommaInMultilineFixer::ELEMENTS_PARAMETERS]],
+        ];
+
+        yield 'match' => [
+            '<?php
+$m = match ($a) {
+    200, 300 => null,
+    400 => 1,
+    500 => function() {return 2;},
+    600 => static function() {return 4;},
+    default => 3,
+};
+
+$z = match ($a) {
+    1 => 0,
+    2 => 1,
+};
+
+$b = match($c) {19 => 28, default => 333};
+            ',
+            '<?php
+$m = match ($a) {
+    200, 300 => null,
+    400 => 1,
+    500 => function() {return 2;},
+    600 => static function() {return 4;},
+    default => 3
+};
+
+$z = match ($a) {
+    1 => 0,
+    2 => 1
+};
+
+$b = match($c) {19 => 28, default => 333};
+            ',
+            ['elements' => ['match']],
         ];
     }
 }