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

Issue:
Case separation between Anonymous functions and Arrow functions
## Feature request

Currently, with the configuration `'function_declaration' => ['closure_function_spacing' => 'none']`, I get the following result.

```
$a = function() {
    return 0;
};
$b = fn() => 0;
```

Instead, with `one` I get the following.

```
$a = function () {
    return 0;
};
$b = fn () => 0;
```

Is there a way to split this cases into two separated cases? Like `anonymous_function_spacing` and `arrow_function_spacing`?
I'm asking because I'm trying to create a .dist.php file that is compatible with PhpStorm PSR1/PSR2 scheme format source,
and its behaviour (in formatting PHP code) seems always to place no spaces between `fn` and `(` in arrow function declaration, and instead places a space between `function` and `(`.
This behaviour would be achievable with something - for example - like this.

```
'function_declaration' => [
    'arrow_function_spacing' => 'none',
    'anonymous_function_spacing' => 'one',
]
```

Pull request:
feature: Add separate option for closure_fn_spacing
Resolves #5916

Notes:
* I'm not sure if `closure_fn_spacing` is the best name for the new option. I tried to make it consistent with the existing option.
* The default is still `one` for the new option. In the next major version the default should be changed to `none` since this is how arrow functions were [intended to be written](https://www.reddit.com/r/PHP/comments/fctb18/comment/fjd9u6a/).

Patch:
diff --git a/dev-tools/composer.lock b/dev-tools/composer.lock
index d82dc1121..979291229 100644
--- a/dev-tools/composer.lock
+++ b/dev-tools/composer.lock
@@ -2201,25 +2201,30 @@
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "1.6.1",
+            "version": "1.6.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "77a32518733312af16a44300404e945338981de3"
+                "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
-                "reference": "77a32518733312af16a44300404e945338981de3",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/48f445a408c131e38cab1c235aa6d2bb7a0bb20d",
+                "reference": "48f445a408c131e38cab1c235aa6d2bb7a0bb20d",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.2 || ^8.0",
+                "php": "^7.4 || ^8.0",
                 "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
                 "ext-tokenizer": "*",
-                "psalm/phar": "^4.8"
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1.8",
+                "phpstan/phpstan-phpunit": "^1.1",
+                "phpunit/phpunit": "^9.5",
+                "rector/rector": "^0.13.9",
+                "vimeo/psalm": "^4.25"
             },
             "type": "library",
             "extra": {
@@ -2245,9 +2250,9 @@
             "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
             "support": {
                 "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
-                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.2"
             },
-            "time": "2022-03-15T21:29:03+00:00"
+            "time": "2022-10-14T12:47:21+00:00"
         },
         {
             "name": "phpmd/phpmd",
@@ -2334,16 +2339,16 @@
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.8.8",
+            "version": "1.8.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "08310ce271984587e2a4cda94e1ac66510a6ea07"
+                "reference": "46e223dd68a620da18855c23046ddb00940b4014"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/08310ce271984587e2a4cda94e1ac66510a6ea07",
-                "reference": "08310ce271984587e2a4cda94e1ac66510a6ea07",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014",
+                "reference": "46e223dd68a620da18855c23046ddb00940b4014",
                 "shasum": ""
             },
             "require": {
@@ -2373,7 +2378,7 @@
             ],
             "support": {
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/1.8.8"
+                "source": "https://github.com/phpstan/phpstan/tree/1.8.11"
             },
             "funding": [
                 {
@@ -2389,25 +2394,25 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-06T12:51:57+00:00"
+            "time": "2022-10-24T15:45:13+00:00"
         },
         {
             "name": "phpstan/phpstan-phpunit",
-            "version": "1.1.1",
+            "version": "1.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan-phpunit.git",
-                "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84"
+                "reference": "09b5c9ab38d4d601bcff04b4c9240832b86f5dda"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/4a3c437c09075736285d1cabb5c75bf27ed0bc84",
-                "reference": "4a3c437c09075736285d1cabb5c75bf27ed0bc84",
+                "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/09b5c9ab38d4d601bcff04b4c9240832b86f5dda",
+                "reference": "09b5c9ab38d4d601bcff04b4c9240832b86f5dda",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2 || ^8.0",
-                "phpstan/phpstan": "^1.5.0"
+                "phpstan/phpstan": "^1.8.11"
             },
             "conflict": {
                 "phpunit/phpunit": "<7.0"
@@ -2439,9 +2444,9 @@
             "description": "PHPUnit extensions and rules for PHPStan",
             "support": {
                 "issues": "https://github.com/phpstan/phpstan-phpunit/issues",
-                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1"
+                "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.2.1"
             },
-            "time": "2022-04-20T15:24:25+00:00"
+            "time": "2022-10-26T19:38:49+00:00"
         },
         {
             "name": "psr/container",
diff --git a/doc/list.rst b/doc/list.rst
index f82ee28e4..75ff0d501 100644
--- a/doc/list.rst
+++ b/doc/list.rst
@@ -805,6 +805,10 @@ List of Available Rules
      | Spacing to use before open parenthesis for closures.
      | Allowed values: ``'none'``, ``'one'``
      | Default value: ``'one'``
+   - | ``closure_fn_spacing``
+     | Spacing to use before open parenthesis for short arrow functions.
+     | Allowed values: ``'none'``, ``'one'``
+     | Default value: ``'one'``
    - | ``trailing_comma_single_line``
      | Whether trailing commas are allowed in single line signatures.
      | Allowed types: ``bool``
diff --git a/doc/rules/function_notation/function_declaration.rst b/doc/rules/function_notation/function_declaration.rst
index cad73364a..c88a4d88d 100644
--- a/doc/rules/function_notation/function_declaration.rst
+++ b/doc/rules/function_notation/function_declaration.rst
@@ -16,6 +16,15 @@ Allowed values: ``'none'``, ``'one'``
 
 Default value: ``'one'``
 
+``closure_fn_spacing``
+~~~~~~~~~~~~~~~~~~~~~~
+
+Spacing to use before open parenthesis for short arrow functions.
+
+Allowed values: ``'none'``, ``'one'``
+
+Default value: ``'one'``
+
 ``trailing_comma_single_line``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -70,7 +79,7 @@ With configuration: ``['closure_function_spacing' => 'none']``.
 Example #3
 ~~~~~~~~~~
 
-With configuration: ``['closure_function_spacing' => 'none']``.
+With configuration: ``['closure_fn_spacing' => 'none']``.
 
 .. code-block:: diff
 
diff --git a/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php b/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php
index 9005f5d9b..e61c42145 100644
--- a/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php
+++ b/src/Fixer/FunctionNotation/FunctionDeclarationFixer.php
@@ -93,7 +93,7 @@ function  foo  ($bar, $baz)
 $f = fn () => null;
 ',
                     new VersionSpecification(70400),
-                    ['closure_function_spacing' => self::SPACING_NONE]
+                    ['closure_fn_spacing' => self::SPACING_NONE]
                 ),
             ]
         );
@@ -195,7 +195,9 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                 $tokens->clearAt($startParenthesisIndex - 1);
             }
 
-            if ($isLambda && self::SPACING_NONE === $this->configuration['closure_function_spacing']) {
+            $option = $token->isGivenKind(T_FN) ? 'closure_fn_spacing' : 'closure_function_spacing';
+
+            if ($isLambda && self::SPACING_NONE === $this->configuration[$option]) {
                 // optionally remove whitespace after T_FUNCTION of a closure
                 // eg: `function () {}` => `function() {}`
                 if ($tokens[$index + 1]->isWhitespace()) {
@@ -229,6 +231,10 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
                 ->setDefault(self::SPACING_ONE)
                 ->setAllowedValues(self::SUPPORTED_SPACINGS)
                 ->getOption(),
+            (new FixerOptionBuilder('closure_fn_spacing', 'Spacing to use before open parenthesis for short arrow functions.'))
+                ->setDefault(self::SPACING_ONE) // @TODO change to SPACING_NONE on next major 4.0
+                ->setAllowedValues(self::SUPPORTED_SPACINGS)
+                ->getOption(),
             (new FixerOptionBuilder('trailing_comma_single_line', 'Whether trailing commas are allowed in single line signatures.'))
                 ->setAllowedTypes(['bool'])
                 ->setDefault(false)


Test patch:
diff --git a/tests/Fixer/FunctionNotation/FunctionDeclarationFixerTest.php b/tests/Fixer/FunctionNotation/FunctionDeclarationFixerTest.php
index 59d26119f..5b52d1e3a 100644
--- a/tests/Fixer/FunctionNotation/FunctionDeclarationFixerTest.php
+++ b/tests/Fixer/FunctionNotation/FunctionDeclarationFixerTest.php
@@ -33,6 +33,11 @@ final class FunctionDeclarationFixerTest extends AbstractFixerTestCase
      */
     private static $configurationClosureSpacingNone = ['closure_function_spacing' => FunctionDeclarationFixer::SPACING_NONE];
 
+    /**
+     * @var array<string,string>
+     */
+    private static $configurationArrowSpacingNone = ['closure_fn_spacing' => FunctionDeclarationFixer::SPACING_NONE];
+
     public function testInvalidConfigurationClosureFunctionSpacing(): void
     {
         $this->expectException(InvalidFixerConfigurationException::class);
@@ -43,6 +48,16 @@ public function testInvalidConfigurationClosureFunctionSpacing(): void
         $this->fixer->configure(['closure_function_spacing' => 'neither']);
     }
 
+    public function testInvalidConfigurationClosureFnSpacing(): void
+    {
+        $this->expectException(InvalidFixerConfigurationException::class);
+        $this->expectExceptionMessageMatches(
+            '#^\[function_declaration\] Invalid configuration: The option "closure_fn_spacing" with value "neither" is invalid\. Accepted values are: "none", "one"\.$#'
+        );
+
+        $this->fixer->configure(['closure_fn_spacing' => 'neither']);
+    }
+
     /**
      * @param array<string, mixed> $configuration
      *
@@ -384,32 +399,32 @@ function foo() /* bar */
             [
                 '<?php fn($i) => null;',
                 null,
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php fn($a) => null;',
                 '<?php fn ($a)      => null;',
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php $fn = fn() => null;',
                 '<?php $fn = fn ()=> null;',
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php $fn("");',
                 null,
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php fn&($a) => null;',
                 '<?php fn &(  $a   ) => null;',
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php fn&($a,$b) => null;',
                 '<?php fn &(  $a,$b  ) => null;',
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
             [
                 '<?php $b = static fn ($a) => $a;',
@@ -418,7 +433,7 @@ function foo() /* bar */
             [
                 '<?php $b = static fn($a) => $a;',
                 '<?php $b = static     fn ( $a )   => $a;',
-                self::$configurationClosureSpacingNone,
+                self::$configurationArrowSpacingNone,
             ],
         ];
     }
@@ -485,7 +500,7 @@ public function provideFixPhp80Cases(): iterable
         yield [
             '<?php fn&($a,$b) => null;',
             '<?php fn &(  $a,$b,   ) => null;',
-            self::$configurationClosureSpacingNone,
+            self::$configurationArrowSpacingNone,
         ];
 
         yield [
diff --git a/tests/Test/AbstractIntegrationTestCase.php b/tests/Test/AbstractIntegrationTestCase.php
index c2db4201c..51bd904c2 100644
--- a/tests/Test/AbstractIntegrationTestCase.php
+++ b/tests/Test/AbstractIntegrationTestCase.php
@@ -261,7 +261,7 @@ protected function doTest(IntegrationCase $case): void
                 "Expected changes do not match result for \"%s\" in \"%s\".\nFixers applied:\n%s.",
                 $case->getTitle(),
                 $case->getFileName(),
-                null === $changed ? '[None]' : implode(',', $changed['appliedFixers'])
+                implode(',', $changed['appliedFixers'])
             )
         );