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

Issue:
NoSuperfluousPhpdocTagsFixer - doesn't work with type alteration
```php
<?php
class Foo {
    /**
     * @var int|string $bar
     */
    private int|string $bar;
}
```

```php
    /**
     * @dataProvider provideFixPhp80Cases
     * @requires PHP 8.0
     */
    public function testFixPhp80(string $expected, ?string $input = null, array $config = []): void
    {
        $this->fixer->configure($config);
        $this->doTest($expected, $input);
    }

    public function provideFixPhp80Cases(): \Generator
    {
        yield [
                '<?php
class Foo {
    /**
     */
    private int|string $bar;
}',
                '<?php
class Foo {
    /**
     * @var int|string $bar
     */
    private int|string $bar;
}',
        ];

        yield 'static return' => [
            '<?php
class Foo {
    /**
     */
    public function foo($foo): static {}
}',
            '<?php
class Foo {
    /**
     * @return static
     */
    public function foo($foo): static {}
}',
        ];
    }
```

Pull request description:
NoSuperfluousPhpdocTagsFixer - Add union types support
Closes #5976.

Patch:
diff --git a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
index e5e630c37..c8b023768 100644
--- a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
+++ b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
@@ -181,7 +181,7 @@ private function findDocumentedElement(Tokens $tokens, int $docCommentIndex): ?i
             }
         } while ($tokens[$index]->isGivenKind([T_ABSTRACT, T_FINAL, T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC]));
 
-        $kindsBeforeProperty = [T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, T_STRING, T_NS_SEPARATOR];
+        $kindsBeforeProperty = [T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, T_STRING, T_NS_SEPARATOR];
 
         if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
             $kindsBeforeProperty[] = T_READONLY;
@@ -293,7 +293,7 @@ private function getArgumentsInfo(Tokens $tokens, int $start, int $end): array
                 $info = $this->parseTypeHint($tokens, $typeIndex);
             } else {
                 $info = [
-                    'type' => null,
+                    'types' => [],
                     'allows_null' => true,
                 ];
             }
@@ -322,7 +322,7 @@ private function getReturnTypeInfo(Tokens $tokens, int $closingParenthesisIndex)
         }
 
         return [
-            'type' => null,
+            'types' => [],
             'allows_null' => true,
         ];
     }
@@ -334,7 +334,7 @@ private function getPropertyTypeInfo(Tokens $tokens, int $index): array
     {
         if ($tokens[$index]->isGivenKind(T_VARIABLE)) {
             return [
-                'type' => null,
+                'types' => [],
                 'allows_null' => true,
             ];
         }
@@ -354,15 +354,31 @@ private function parseTypeHint(Tokens $tokens, int $index): array
             $index = $tokens->getNextMeaningfulToken($index);
         }
 
-        $type = '';
+        $types = [];
+
+        while (true) {
+            $type = '';
+
+            while ($tokens[$index]->isGivenKind([T_NS_SEPARATOR, T_STATIC, T_STRING, CT::T_ARRAY_TYPEHINT, T_CALLABLE])) {
+                $type .= $tokens[$index]->getContent();
+                $index = $tokens->getNextMeaningfulToken($index);
+            }
+
+            if ('' === $type) {
+                break;
+            }
+
+            $types[] = $type;
+
+            if (!$tokens[$index]->isGivenKind(CT::T_TYPE_ALTERNATION)) {
+                break;
+            }
 
-        while ($tokens[$index]->isGivenKind([T_NS_SEPARATOR, T_STATIC, T_STRING, CT::T_ARRAY_TYPEHINT, T_CALLABLE])) {
-            $type .= $tokens[$index]->getContent();
             $index = $tokens->getNextMeaningfulToken($index);
         }
 
         return [
-            'type' => '' === $type ? null : $type,
+            'types' => $types,
             'allows_null' => $allowsNull,
         ];
     }
@@ -390,11 +406,11 @@ private function annotationIsSuperfluous(Annotation $annotation, array $info, ar
             return false;
         }
 
-        if (['mixed'] === $annotationTypes && null === $info['type']) {
+        if (['mixed'] === $annotationTypes && [] === $info['types']) {
             return !$this->configuration['allow_mixed'];
         }
 
-        $actualTypes = null === $info['type'] ? [] : [$info['type']];
+        $actualTypes = $info['types'];
 
         if ($info['allows_null']) {
             $actualTypes[] = 'null';


Test patch:
diff --git a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
index 1ed5a34e3..c8868a30f 100644
--- a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
+++ b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
@@ -254,7 +254,7 @@ function foo(Bar $bar, \Foo\Baz $baz) {}',
  */
 function foo(Bar $bar, \Foo\Baz $baz) {}',
             ],
-            'with aliased imported' => [
+            'with aliased import' => [
                 '<?php
 use Foo\Bar as Baz;
 
@@ -1174,7 +1174,7 @@ function foo(Bar $bar, \Foo\Baz $baz): \Foo\Qux {}',
  */
 function foo(Bar $bar, \Foo\Baz $baz): \Foo\Qux {}',
             ],
-            'with aliased imported (with return type)' => [
+            'with aliased import (with return type)' => [
                 '<?php
 use Foo\Bar as Baz;
 
@@ -1247,21 +1247,6 @@ trait Foo {}',
 trait Foo {}',
                 ['remove_inheritdoc' => true],
             ],
-        ];
-    }
-
-    /**
-     * @dataProvider provideFixPhp71Cases
-     * @requires PHP 7.1
-     */
-    public function testFixPhp71(string $expected, ?string $input = null): void
-    {
-        $this->doTest($expected, $input);
-    }
-
-    public function provideFixPhp71Cases(): array
-    {
-        return [
             'same nullable type hint' => [
                 '<?php
 class Foo {
@@ -1324,7 +1309,7 @@ class Foo {
     public function doFoo(?Bar $bar = null) {}
 }',
             ],
-            'multiple different types' => [
+            'multiple different types (nullable)' => [
                 '<?php
 class Foo {
     /**
@@ -1335,7 +1320,7 @@ class Foo {
     public function doFoo(?Bar $bar): ?Baz {}
 }',
             ],
-            'with import' => [
+            'with nullable import' => [
                 '<?php
 use Foo\Bar;
 use Foo\Baz;
@@ -1353,7 +1338,7 @@ function foo(?Bar $bar): ?Baz {}',
  */
 function foo(?Bar $bar): ?Baz {}',
             ],
-            'with root symbols' => [
+            'with nullable root symbols' => [
                 '<?php
 /**
  */
@@ -1365,7 +1350,7 @@ function foo(?\Foo\Bar $bar): ?\Foo\Baz {}',
  */
 function foo(?\Foo\Bar $bar): ?\Foo\Baz {}',
             ],
-            'with mix of imported and fully qualified symbols' => [
+            'with nullable mix of imported and fully qualified symbols' => [
                 '<?php
 use Foo\Bar;
 use Foo\Baz;
@@ -1386,7 +1371,7 @@ function foo(?Bar $bar, ?\Foo\Baz $baz): ?\Foo\Qux {}',
  */
 function foo(?Bar $bar, ?\Foo\Baz $baz): ?\Foo\Qux {}',
             ],
-            'with aliased imported' => [
+            'with nullable aliased import' => [
                 '<?php
 use Foo\Bar as Baz;
 
@@ -1402,7 +1387,7 @@ function foo(?Baz $bar): ?Baz {}',
  */
 function foo(?Baz $bar): ?Baz {}',
             ],
-            'with special type hints' => [
+            'with nullable special type hints' => [
                 '<?php
 class Foo {
     /**
@@ -1728,6 +1713,94 @@ class Foo {
     public function foo($foo): static {}
 }',
             ],
+            'union type on parameter' => [
+                '<?php
+class Foo {
+    /**
+     */
+    public function foo(int|string $foo) {}
+}',
+                '<?php
+class Foo {
+    /**
+     * @param int|string $foo
+     */
+    public function foo(int|string $foo) {}
+}',
+            ],
+            'union type on return type' => [
+                '<?php
+class Foo {
+    /**
+     */
+    public function foo($foo): int|string {}
+}',
+                '<?php
+class Foo {
+    /**
+     * @return int|string
+     */
+    public function foo($foo): int|string {}
+}',
+            ],
+            'union type on property' => [
+                '<?php
+class Foo {
+    /**
+     */
+    public int|string $foo;
+}',
+                '<?php
+class Foo {
+    /**
+     * @var int|string
+     */
+    public int|string $foo;
+}',
+            ],
+            'union type with null' => [
+                '<?php
+/**
+ */
+function foo(int|string|null $foo) {}',
+                '<?php
+/**
+ * @param int|string|null $foo
+ */
+function foo(int|string|null $foo) {}',
+            ],
+            'union type in different order' => [
+                '<?php
+/**
+ */
+function foo(string|int $foo) {}',
+                '<?php
+/**
+ * @param int|string $foo
+ */
+function foo(string|int $foo) {}',
+            ],
+            'more details in phpdocs' => [
+                '<?php
+/**
+ * @param string|array<string> $foo
+ */
+function foo(string|array $foo) {}',
+            ],
+            'missing types in phpdocs' => [
+                '<?php
+/**
+ * @param string|int $foo
+ */
+function foo(string|array|int $foo) {}',
+            ],
+            'too many types in phpdocs' => [
+                '<?php
+/**
+ * @param string|array|int $foo
+ */
+function foo(string|int $foo) {}',
+            ],
         ];
     }