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

Issue:
PHP-CS-Fixer 3.15.0 wants to add spaces around | in type declarations for parameters that are passed by reference
## Bug report

### Description

After updating from 3.14.4 to 3.15.0, PHP-CS-Fixer wants to change `object|array` to `object | array`.

### Runtime version

```
./tools/php-cs-fixer -V                       
PHP CS Fixer 3.15.0 (7306744) BoY42 by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.2.3
```

### Used command

```
./tools/php-cs-fixer fix --dry-run --diff
Loaded config default from "/usr/local/src/recursion-context/.php-cs-fixer.dist.php".
Using cache file ".php-cs-fixer.cache".
   1) src/Context.php
      ---------- begin diff ----------
--- /usr/local/src/recursion-context/src/Context.php
+++ /usr/local/src/recursion-context/src/Context.php
@@ -50,7 +50,7 @@
      *
      * @param-out T $value
      */
-    public function add(object|array &$value): int|string|false
+    public function add(object | array &$value): int|string|false
     {
         if (is_array($value)) {
             return $this->addArray($value);
@@ -66,7 +66,7 @@
      *
      * @param-out T $value
      */
-    public function contains(object|array &$value): int|string|false
+    public function contains(object | array &$value): int|string|false
     {
         if (is_array($value)) {
             return $this->containsArray($value);

      ----------- end diff -----------
```

### Configuration file

https://github.com/sebastianbergmann/recursion-context/blob/main/.php-cs-fixer.dist.php



Discussion:
Similar issue in PHPUnit:

```
./tools/php-cs-fixer fix --dry-run --diff
Loaded config default from "/usr/local/src/phpunit/.php-cs-fixer.dist.php".
Using cache file ".php-cs-fixer.cache".
   1) tests/_files/mock-object/ClassWithStaticReturnTypes.php
      ---------- begin diff ----------
--- /usr/local/src/phpunit/tests/_files/mock-object/ClassWithStaticReturnTypes.php
+++ /usr/local/src/phpunit/tests/_files/mock-object/ClassWithStaticReturnTypes.php
@@ -21,7 +21,7 @@
     {
     }
 
-    public function returnsUnionWithStatic(): static|stdClass
+    public function returnsUnionWithStatic(): static | stdClass
     {
     }
 }

      ----------- end diff -----------


Found 1 of 1421 files that can be fixed in 0.071 seconds, 20.574 MB memory used
```

https://github.com/sebastianbergmann/phpunit/blob/main/.php-cs-fixer.dist.php
Looks like a regression of #5495 / #5733 to me.
Just noticed #6826 which seems to be about the same issue shown in https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/6828#issue-1620840606 (parameters with union types passed by reference).

#6826 does not seem to be related to the issue shown in https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/6828#issuecomment-1465587966 (return type declaration without reference operator).
@sebastianbergmann wouldn't it be problem for you to close this issue and create new one, just with the example without [passing by reference](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/6828#issuecomment-1465587966)? Or edit this one to keep only scenario not covered by #6826? This would be easier to process, thanks in advance. PS. We'll look at it anyway 🙂
I moved https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/6828#issuecomment-1465587966 to #6830.
Thank you!



Patch:
diff --git a/src/Tokenizer/AbstractTypeTransformer.php b/src/Tokenizer/AbstractTypeTransformer.php
index dc7220c79a3..dc26e5b2743 100644
--- a/src/Tokenizer/AbstractTypeTransformer.php
+++ b/src/Tokenizer/AbstractTypeTransformer.php
@@ -50,18 +50,24 @@ protected function doProcess(Tokens $tokens, int $index, $originalToken): void
 
     private function isPartOfType(Tokens $tokens, int $index): bool
     {
-        // for parameter there will be variable after type
-        $variableIndex = $tokens->getTokenNotOfKindSibling($index, 1, self::TYPE_TOKENS);
-        if ($tokens[$variableIndex]->isGivenKind(T_VARIABLE)) {
-            return $tokens[$tokens->getPrevMeaningfulToken($variableIndex)]->equalsAny(self::TYPE_END_TOKENS);
-        }
-
         // return types and non-capturing catches
         $typeColonIndex = $tokens->getTokenNotOfKindSibling($index, -1, self::TYPE_TOKENS);
         if ($tokens[$typeColonIndex]->isGivenKind([T_CATCH, CT::T_TYPE_COLON])) {
             return true;
         }
 
-        return false;
+        // for parameter there will be variable or reference after type
+        $variableIndex = $tokens->getTokenNotOfKindSibling($index, 1, self::TYPE_TOKENS);
+        if (!$tokens[$variableIndex]->isGivenKind(T_VARIABLE)) {
+            return false;
+        }
+        $beforeVariableIndex = $tokens->getPrevMeaningfulToken($variableIndex);
+        if ($tokens[$beforeVariableIndex]->equals('&')) {
+            $prevIndex = $tokens->getPrevTokenOfKind($index, ['{', '}', ';', [T_FN], [T_FUNCTION]]);
+
+            return null !== $prevIndex && $tokens[$prevIndex]->isGivenKind([T_FN, T_FUNCTION]);
+        }
+
+        return $tokens[$beforeVariableIndex]->equalsAny(self::TYPE_END_TOKENS);
     }
 }


Test patch:
diff --git a/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php b/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
index 2318c45653f..43b87daefd6 100644
--- a/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
+++ b/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
@@ -3107,6 +3107,9 @@ public function baz(
                     callable|array $a,
                     array|callable $b,
                 ) {}
+                public function qux(
+                    bool|int | string &$reference
+                ) {}
             }'
         );
     }
diff --git a/tests/Tokenizer/TokensAnalyzerTest.php b/tests/Tokenizer/TokensAnalyzerTest.php
index 3a710c65e7c..faf1780d033 100644
--- a/tests/Tokenizer/TokensAnalyzerTest.php
+++ b/tests/Tokenizer/TokensAnalyzerTest.php
@@ -1878,6 +1878,11 @@ public static function provideIsBinaryOperator80Cases(): iterable
             [6 => false],
             '<?php function foo(callable|int $x) {}',
         ];
+
+        yield [
+            [6 => false],
+            '<?php function foo(bool|int &$x) {}',
+        ];
     }
 
     /**
diff --git a/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php b/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
index 0e28bc5b754..60345d8901a 100644
--- a/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
+++ b/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
@@ -388,6 +388,25 @@ public function __construct(
                 ) {}
             }',
         ];
+
+        yield 'parameters by reference' => [
+            [
+                19 => CT::T_TYPE_ALTERNATION,
+                21 => CT::T_TYPE_ALTERNATION,
+                91 => CT::T_TYPE_ALTERNATION,
+                93 => CT::T_TYPE_ALTERNATION,
+            ],
+            '<?php
+                f(FOO|BAR|BAZ&$x);
+                function f1(FOO|BAR|BAZ&$x) {} // Alternation found
+                function f2(FOO&BAR&BAZ&$x) {}
+                f(FOO&BAR|BAZ&$x);
+                f(FOO|BAR&BAZ&$x);
+                fn(FOO&BAR&BAZ&$x) => 0;
+                fn(FOO|BAR|BAZ&$x) => 0; // Alternation found
+                f(FOO&BAR&BAZ&$x);
+            ',
+        ];
     }
 
     /**
diff --git a/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php b/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
index 7be518b961d..33fc9173da8 100644
--- a/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
+++ b/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
@@ -323,6 +323,25 @@ function f( #[Target(\'a\')] #[Target(\'b\')] #[Target(\'c\')] #[Target(\'d\')]
                 45 => CT::T_TYPE_INTERSECTION,
             ],
         ];
+
+        yield 'parameters by reference' => [
+            '<?php
+                f(FOO|BAR|BAZ&$x);
+                function f1(FOO|BAR|BAZ&$x) {}
+                function f2(FOO&BAR&BAZ&$x) {} // Intersection found
+                f(FOO&BAR|BAZ&$x);
+                f(FOO|BAR&BAZ&$x);
+                fn(FOO&BAR&BAZ&$x) => 0; // Intersection found
+                fn(FOO|BAR|BAZ&$x) => 0;
+                f(FOO&BAR&BAZ&$x);
+            ',
+            [
+                35 => CT::T_TYPE_INTERSECTION,
+                37 => CT::T_TYPE_INTERSECTION,
+                75 => CT::T_TYPE_INTERSECTION,
+                77 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
     }
 
     /**