Просмотр исходного кода

feature: Introduce `TypeDeclarationSpacesFixer` (#7001)

Co-authored-by: Kuba Werłos <werlos@gmail.com>
John Paul E. Balandan, CPA 1 год назад
Родитель
Сommit
d7b5c39357

+ 16 - 1
doc/list.rst

@@ -879,7 +879,7 @@ List of Available Rules
 
    Ensure single space between function's argument and its typehint.
 
-   Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
+   *warning deprecated*   Use ``type_declaration_spaces`` instead.
 
    `Source PhpCsFixer\\Fixer\\FunctionNotation\\FunctionTypehintSpaceFixer <./../src/Fixer/FunctionNotation/FunctionTypehintSpaceFixer.php>`_
 -  `general_phpdoc_annotation_remove <./rules/phpdoc/general_phpdoc_annotation_remove.rst>`_
@@ -3194,6 +3194,21 @@ List of Available Rules
    Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 
    `Source PhpCsFixer\\Fixer\\Whitespace\\TypesSpacesFixer <./../src/Fixer/Whitespace/TypesSpacesFixer.php>`_
+-  `type_declaration_spaces <./rules/whitespace/type_declaration_spaces.rst>`_
+
+   Ensure single space between a variable and its type declaration in function arguments and properties.
+
+   Configuration options:
+
+   - | ``elements``
+     | Structural elements where the spacing after the type declaration should be fixed.
+     | Allowed values: a subset of ``['function', 'property']``
+     | Default value: ``['function', 'property']``
+
+
+   Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
+
+   `Source PhpCsFixer\\Fixer\\Whitespace\\TypeDeclarationSpacesFixer <./../src/Fixer/Whitespace/TypeDeclarationSpacesFixer.php>`_
 -  `unary_operator_spaces <./rules/operator/unary_operator_spaces.rst>`_
 
    Unary operators should be placed adjacent to their operands.

+ 1 - 1
doc/ruleSets/Symfony.rst

@@ -35,7 +35,6 @@ Rules
   ``['style' => 'braces']``
 - `empty_loop_condition <./../rules/control_structure/empty_loop_condition.rst>`_
 - `fully_qualified_strict_types <./../rules/import/fully_qualified_strict_types.rst>`_
-- `function_typehint_space <./../rules/function_notation/function_typehint_space.rst>`_
 - `general_phpdoc_tag_rename <./../rules/phpdoc/general_phpdoc_tag_rename.rst>`_
   config:
   ``['replacements' => ['inheritDocs' => 'inheritDoc']]``
@@ -146,6 +145,7 @@ Rules
 - `switch_continue_to_break <./../rules/control_structure/switch_continue_to_break.rst>`_
 - `trailing_comma_in_multiline <./../rules/control_structure/trailing_comma_in_multiline.rst>`_
 - `trim_array_spaces <./../rules/array_notation/trim_array_spaces.rst>`_
+- `type_declaration_spaces <./../rules/whitespace/type_declaration_spaces.rst>`_
 - `types_spaces <./../rules/whitespace/types_spaces.rst>`_
 - `unary_operator_spaces <./../rules/operator/unary_operator_spaces.rst>`_
 - `whitespace_after_comma_in_array <./../rules/array_notation/whitespace_after_comma_in_array.rst>`_

+ 8 - 11
doc/rules/function_notation/function_typehint_space.rst

@@ -4,6 +4,14 @@ Rule ``function_typehint_space``
 
 Ensure single space between function's argument and its typehint.
 
+Warning
+-------
+
+This rule is deprecated and will be removed on next major version
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You should use ``type_declaration_spaces`` instead.
+
 Examples
 --------
 
@@ -30,14 +38,3 @@ Example #2
    -function sample(array  $a)
    +function sample(array $a)
     {}
-
-Rule sets
----------
-
-The rule is part of the following rule sets:
-
-@PhpCsFixer
-  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``function_typehint_space`` rule.
-
-@Symfony
-  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``function_typehint_space`` rule.

+ 4 - 1
doc/rules/index.rst

@@ -343,7 +343,7 @@ Function Notation
 - `function_declaration <./function_notation/function_declaration.rst>`_
 
   Spaces should be properly placed in a function declaration.
-- `function_typehint_space <./function_notation/function_typehint_space.rst>`_
+- `function_typehint_space <./function_notation/function_typehint_space.rst>`_ *(deprecated)*
 
   Ensure single space between function's argument and its typehint.
 - `implode_call <./function_notation/implode_call.rst>`_ *(risky)*
@@ -885,6 +885,9 @@ Whitespace
 - `statement_indentation <./whitespace/statement_indentation.rst>`_
 
   Each statement must be indented.
+- `type_declaration_spaces <./whitespace/type_declaration_spaces.rst>`_
+
+  Ensure single space between a variable and its type declaration in function arguments and properties.
 - `types_spaces <./whitespace/types_spaces.rst>`_
 
   A single space or none should be around union type and intersection type operators.

+ 94 - 0
doc/rules/whitespace/type_declaration_spaces.rst

@@ -0,0 +1,94 @@
+================================
+Rule ``type_declaration_spaces``
+================================
+
+Ensure single space between a variable and its type declaration in function
+arguments and properties.
+
+Configuration
+-------------
+
+``elements``
+~~~~~~~~~~~~
+
+Structural elements where the spacing after the type declaration should be
+fixed.
+
+Allowed values: a subset of ``['function', 'property']``
+
+Default value: ``['function', 'property']``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+*Default* configuration.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class Bar
+    {
+   -    private string    $a;
+   -    private bool   $b;
+   +    private string $a;
+   +    private bool $b;
+
+   -    public function __invoke(array   $c) {}
+   +    public function __invoke(array $c) {}
+    }
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['elements' => ['function']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class Foo
+    {
+        public int   $bar;
+
+   -    public function baz(string     $a)
+   +    public function baz(string $a)
+        {
+   -        return fn(bool    $c): string => (string) $c;
+   +        return fn(bool $c): string => (string) $c;
+        }
+    }
+
+Example #3
+~~~~~~~~~~
+
+With configuration: ``['elements' => ['property']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class Foo
+    {
+   -    public int   $bar;
+   +    public int $bar;
+
+        public function baz(string     $a) {}
+    }
+
+Rule sets
+---------
+
+The rule is part of the following rule sets:
+
+@PhpCsFixer
+  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``type_declaration_spaces`` rule with the default config.
+
+@Symfony
+  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``type_declaration_spaces`` rule with the default config.

+ 14 - 24
src/Fixer/FunctionNotation/FunctionTypehintSpaceFixer.php

@@ -14,18 +14,20 @@ declare(strict_types=1);
 
 namespace PhpCsFixer\Fixer\FunctionNotation;
 
-use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\AbstractProxyFixer;
+use PhpCsFixer\Fixer\DeprecatedFixerInterface;
+use PhpCsFixer\Fixer\Whitespace\TypeDeclarationSpacesFixer;
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
-use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
-use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
 use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @deprecated
  */
-final class FunctionTypehintSpaceFixer extends AbstractFixer
+final class FunctionTypehintSpaceFixer extends AbstractProxyFixer implements DeprecatedFixerInterface
 {
     public function getDefinition(): FixerDefinitionInterface
     {
@@ -43,28 +45,16 @@ final class FunctionTypehintSpaceFixer extends AbstractFixer
         return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_FN]);
     }
 
-    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    public function getSuccessorsNames(): array
     {
-        $functionsAnalyzer = new FunctionsAnalyzer();
-
-        for ($index = $tokens->count() - 1; $index >= 0; --$index) {
-            $token = $tokens[$index];
-
-            if (!$token->isGivenKind([T_FUNCTION, T_FN])) {
-                continue;
-            }
-
-            $arguments = $functionsAnalyzer->getFunctionArguments($tokens, $index);
-
-            foreach (array_reverse($arguments) as $argument) {
-                $type = $argument->getTypeAnalysis();
+        return array_keys($this->proxyFixers);
+    }
 
-                if (!$type instanceof TypeAnalysis) {
-                    continue;
-                }
+    protected function createProxyFixers(): array
+    {
+        $fixer = new TypeDeclarationSpacesFixer();
+        $fixer->configure(['elements' => ['function']]);
 
-                $tokens->ensureWhitespaceAtIndex($type->getEndIndex() + 1, 0, ' ');
-            }
-        }
+        return [$fixer];
     }
 }

+ 199 - 0
src/Fixer/Whitespace/TypeDeclarationSpacesFixer.php

@@ -0,0 +1,199 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Fixer\Whitespace;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
+ */
+final class TypeDeclarationSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Ensure single space between a variable and its type declaration in function arguments and properties.',
+            [
+                new CodeSample(
+                    '<?php
+class Bar
+{
+    private string    $a;
+    private bool   $b;
+
+    public function __invoke(array   $c) {}
+}
+'
+                ),
+                new CodeSample(
+                    '<?php
+class Foo
+{
+    public int   $bar;
+
+    public function baz(string     $a)
+    {
+        return fn(bool    $c): string => (string) $c;
+    }
+}
+',
+                    ['elements' => ['function']]
+                ),
+                new CodeSample(
+                    '<?php
+class Foo
+{
+    public int   $bar;
+
+    public function baz(string     $a) {}
+}
+',
+                    ['elements' => ['property']]
+                ),
+            ]
+        );
+    }
+
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return $tokens->isAnyTokenKindsFound([...Token::getClassyTokenKinds(), T_FN, T_FUNCTION]);
+    }
+
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('elements', 'Structural elements where the spacing after the type declaration should be fixed.'))
+                ->setAllowedTypes(['array'])
+                ->setAllowedValues([new AllowedValueSubset(['function', 'property'])])
+                ->setDefault(['function', 'property'])
+                ->getOption(),
+        ]);
+    }
+
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        $functionsAnalyzer = new FunctionsAnalyzer();
+
+        foreach (array_reverse($this->getElements($tokens), true) as $index => $type) {
+            if ('property' === $type && \in_array('property', $this->configuration['elements'], true)) {
+                $this->ensureSingleSpaceAtPropertyTypehint($tokens, $index);
+
+                continue;
+            }
+
+            if ('method' === $type && \in_array('function', $this->configuration['elements'], true)) {
+                $this->ensureSingleSpaceAtFunctionArgumentTypehint($functionsAnalyzer, $tokens, $index);
+
+                // implicit continue;
+            }
+        }
+    }
+
+    /**
+     * @return array<int, string>
+     *
+     * @phpstan-return array<int, 'method'|'property'>
+     */
+    private function getElements(Tokens $tokens): array
+    {
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+
+        $elements = array_map(
+            static fn (array $element): string => $element['type'],
+            array_filter(
+                $tokensAnalyzer->getClassyElements(),
+                static fn (array $element): bool => \in_array($element['type'], ['method', 'property'], true)
+            )
+        );
+
+        foreach ($tokens as $index => $token) {
+            if (
+                $token->isGivenKind(T_FN)
+                || ($token->isGivenKind(T_FUNCTION) && !isset($elements[$index]))
+            ) {
+                $elements[$index] = 'method';
+            }
+        }
+
+        return $elements;
+    }
+
+    private function ensureSingleSpaceAtFunctionArgumentTypehint(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index): void
+    {
+        foreach (array_reverse($functionsAnalyzer->getFunctionArguments($tokens, $index)) as $argumentInfo) {
+            $argumentType = $argumentInfo->getTypeAnalysis();
+
+            if (null === $argumentType) {
+                continue;
+            }
+
+            $tokens->ensureWhitespaceAtIndex($argumentType->getEndIndex() + 1, 0, ' ');
+        }
+    }
+
+    private function ensureSingleSpaceAtPropertyTypehint(Tokens $tokens, int $index): void
+    {
+        $propertyIndex = $index;
+        $propertyModifiers = [T_PRIVATE, T_PROTECTED, T_PUBLIC, T_STATIC, T_VAR];
+
+        if (\defined('T_READONLY')) {
+            $propertyModifiers[] = T_READONLY; // @TODO drop condition when PHP 8.1 is supported
+        }
+
+        do {
+            $index = $tokens->getPrevMeaningfulToken($index);
+        } while (!$tokens[$index]->isGivenKind($propertyModifiers));
+
+        $propertyType = $this->collectTypeAnalysis($tokens, $index, $propertyIndex);
+
+        if (null === $propertyType) {
+            return;
+        }
+
+        $tokens->ensureWhitespaceAtIndex($propertyType->getEndIndex() + 1, 0, ' ');
+    }
+
+    private function collectTypeAnalysis(Tokens $tokens, int $startIndex, int $endIndex): ?TypeAnalysis
+    {
+        $type = '';
+        $typeStartIndex = $tokens->getNextMeaningfulToken($startIndex);
+        $typeEndIndex = $typeStartIndex;
+
+        for ($i = $typeStartIndex; $i < $endIndex; ++$i) {
+            if ($tokens[$i]->isWhitespace() || $tokens[$i]->isComment()) {
+                continue;
+            }
+
+            $type .= $tokens[$i]->getContent();
+            $typeEndIndex = $i;
+        }
+
+        return '' !== $type ? new TypeAnalysis($type, $typeStartIndex, $typeEndIndex) : null;
+    }
+}

+ 1 - 1
src/RuleSet/Sets/SymfonySet.php

@@ -55,7 +55,6 @@ final class SymfonySet extends AbstractRuleSetDescription
             'empty_loop_body' => ['style' => 'braces'],
             'empty_loop_condition' => true,
             'fully_qualified_strict_types' => true,
-            'function_typehint_space' => true,
             'general_phpdoc_tag_rename' => [
                 'replacements' => [
                     'inheritDocs' => 'inheritDoc',
@@ -204,6 +203,7 @@ final class SymfonySet extends AbstractRuleSetDescription
             'switch_continue_to_break' => true,
             'trailing_comma_in_multiline' => true,
             'trim_array_spaces' => true,
+            'type_declaration_spaces' => true,
             'types_spaces' => true,
             'unary_operator_spaces' => true,
             'whitespace_after_comma_in_array' => true,

+ 400 - 0
tests/Fixer/Whitespace/TypeDeclarationSpacesFixerTest.php

@@ -0,0 +1,400 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Fixer\Whitespace;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\Whitespace\TypeDeclarationSpacesFixer
+ */
+final class TypeDeclarationSpacesFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     */
+    public function testFix(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array<int, null|string>>
+     */
+    public static function provideFixCases(): iterable
+    {
+        yield from [
+            [
+                '<?php function foo(bool /**bla bla*/$param) {}',
+                '<?php function foo(bool/**bla bla*/$param) {}',
+            ],
+            [
+                '<?php function foo(bool /**bla bla*/$param) {}',
+                '<?php function foo(bool  /**bla bla*/$param) {}',
+            ],
+            [
+                '<?php function foo(callable $param) {}',
+                '<?php function foo(callable$param) {}',
+            ],
+            [
+                '<?php function foo(callable $param) {}',
+                '<?php function foo(callable  $param) {}',
+            ],
+            [
+                '<?php function foo(array &$param) {}',
+                '<?php function foo(array&$param) {}',
+            ],
+            [
+                '<?php function foo(array &$param) {}',
+                '<?php function foo(array  &$param) {}',
+            ],
+            [
+                '<?php function foo(array & $param) {}',
+                '<?php function foo(array& $param) {}',
+            ],
+            [
+                '<?php function foo(array & $param) {}',
+                '<?php function foo(array  & $param) {}',
+            ],
+            [
+                '<?php function foo(Bar $param) {}',
+                '<?php function foo(Bar$param) {}',
+            ],
+            [
+                '<?php function foo(Bar $param) {}',
+                '<?php function foo(Bar  $param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz $param) {}',
+                '<?php function foo(Bar\Baz$param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz $param) {}',
+                '<?php function foo(Bar\Baz  $param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz &$param) {}',
+                '<?php function foo(Bar\Baz&$param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz &$param) {}',
+                '<?php function foo(Bar\Baz  &$param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz & $param) {}',
+                '<?php function foo(Bar\Baz& $param) {}',
+            ],
+            [
+                '<?php function foo(Bar\Baz & $param) {}',
+                '<?php function foo(Bar\Baz  & $param) {}',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz $param) {};',
+                '<?php $foo = function(Bar\Baz$param) {};',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz $param) {};',
+                '<?php $foo = function(Bar\Baz  $param) {};',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz &$param) {};',
+                '<?php $foo = function(Bar\Baz&$param) {};',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz &$param) {};',
+                '<?php $foo = function(Bar\Baz  &$param) {};',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz & $param) {};',
+                '<?php $foo = function(Bar\Baz& $param) {};',
+            ],
+            [
+                '<?php $foo = function(Bar\Baz & $param) {};',
+                '<?php $foo = function(Bar\Baz  & $param) {};',
+            ],
+            [
+                '<?php class Test { public function foo(Bar\Baz $param) {} }',
+                '<?php class Test { public function foo(Bar\Baz$param) {} }',
+            ],
+            [
+                '<?php class Test { public function foo(Bar\Baz $param) {} }',
+                '<?php class Test { public function foo(Bar\Baz  $param) {} }',
+            ],
+            [
+                '<?php $foo = function(array $a,
+                    array $b, array $c, array $d) {};',
+                '<?php $foo = function(array $a,
+                    array$b, array     $c, array
+                    $d) {};',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz $param) => null;',
+                '<?php $foo = fn(Bar\Baz$param) => null;',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz $param) => null;',
+                '<?php $foo = fn(Bar\Baz  $param) => null;',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz &$param) => null;',
+                '<?php $foo = fn(Bar\Baz&$param) => null;',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz &$param) => null;',
+                '<?php $foo = fn(Bar\Baz  &$param) => null;',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz & $param) => null;',
+                '<?php $foo = fn(Bar\Baz& $param) => null;',
+            ],
+            [
+                '<?php $foo = fn(Bar\Baz & $param) => null;',
+                '<?php $foo = fn(Bar\Baz  & $param) => null;',
+            ],
+            [
+                '<?php $foo = fn(array $a,
+                    array $b, array $c, array $d) => null;',
+                '<?php $foo = fn(array $a,
+                    array$b, array     $c, array
+                    $d) => null;',
+            ],
+            [
+                '<?php function foo(array ...$param) {}',
+                '<?php function foo(array...$param) {}',
+            ],
+            [
+                '<?php function foo(array & ...$param) {}',
+                '<?php function foo(array& ...$param) {}',
+            ],
+            [
+                '<?php class Foo { public int $x; }',
+                '<?php class Foo { public int$x; }',
+            ],
+            [
+                '<?php class Foo { public bool $x; }',
+                '<?php class Foo { public bool    $x; }',
+            ],
+            [
+                '<?php class Foo { protected \Bar\Baz $c; }',
+                '<?php class Foo { protected \Bar\Baz$c; }',
+            ],
+            [
+                '<?php class Foo { protected \Bar\Baz $c; }',
+                '<?php class Foo { protected \Bar\Baz   $c; }',
+            ],
+            [
+                '<?php class Foo { private array $x; }',
+                '<?php class Foo { private array$x; }',
+            ],
+            [
+                '<?php class Foo { private array $x; }',
+                '<?php class Foo { private array
+$x; }',
+            ],
+            [
+                '<?php
+class Point
+{
+    public \DateTime $x;
+    protected bool $y = true;
+    private array $z = [];
+    public int $a = 0;
+    protected string $b = \'\';
+    private float $c = 0.0;
+}
+',
+                '<?php
+class Point
+{
+    public \DateTime    $x;
+    protected bool      $y = true;
+    private array       $z = [];
+    public int          $a = 0;
+    protected string    $b = \'\';
+    private float       $c = 0.0;
+}
+',
+            ],
+            [
+                '<?php function foo($param) {}',
+            ],
+            [
+                '<?php function foo($param1,$param2) {}',
+            ],
+            [
+                '<?php function foo(&$param) {}',
+            ],
+            [
+                '<?php function foo(& $param) {}',
+            ],
+            [
+                '<?php function foo(/**int*/$param) {}',
+            ],
+            [
+                '<?php function foo(bool /**bla bla*/ $param) {}',
+            ],
+            [
+                '<?php $foo = function(
+                    array $a,
+                    $b
+                ) {};',
+            ],
+            [
+                '<?php $foo = function(
+                    $a,
+                    array $b
+                ) {};',
+            ],
+            [
+                '<?php function foo(...$param) {}',
+            ],
+            [
+                '<?php function foo(&...$param) {}',
+            ],
+            [
+                '<?php use function some\test\{fn_a, fn_b, fn_c};',
+            ],
+            [
+                '<?php use function some\test\{fn_a, fn_b, fn_c} ?>',
+            ],
+            [
+                '<?php $foo = fn(
+                    array $a,
+                    $b
+                ) => null;',
+            ],
+            [
+                '<?php $foo = fn(
+                    $a,
+                    array $b
+                ) => null;',
+            ],
+            [
+                '<?php class Foo { public $p; }',
+            ],
+            [
+                '<?php class Foo { protected /* int */ $a; }',
+            ],
+            [
+                '<?php class Foo { private int $a; }',
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider provideFixPhp80Cases
+     *
+     * @requires PHP 8.0
+     */
+    public function testFixPhp80(string $expected, string $input): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array<int, string>>
+     */
+    public static function provideFixPhp80Cases(): iterable
+    {
+        yield [
+            '<?php function foo(mixed $a) {}',
+            '<?php function foo(mixed$a) {}',
+        ];
+
+        yield [
+            '<?php function foo(mixed $a) {}',
+            '<?php function foo(mixed    $a) {}',
+        ];
+
+        yield [
+            '<?php
+class Foo
+{
+    public function __construct(
+        public int $a,
+        protected bool $b,
+        private Bar\Baz $c,
+    ) {}
+}
+',
+            '<?php
+class Foo
+{
+    public function __construct(
+        public int  $a,
+        protected bool$b,
+        private Bar\Baz     $c,
+    ) {}
+}
+',
+        ];
+    }
+
+    /**
+     * @dataProvider provideFixPhp81Cases
+     *
+     * @requires PHP 8.1
+     */
+    public function testFixPhp81(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array<int, string>>
+     */
+    public static function provideFixPhp81Cases(): iterable
+    {
+        yield [
+            '<?php class Foo { private readonly int $bar; }',
+            '<?php class Foo { private readonly int$bar; }',
+        ];
+
+        yield [
+            '<?php class Foo { private readonly int $bar; }',
+            '<?php class Foo { private readonly int    $bar; }',
+        ];
+    }
+
+    /**
+     * @dataProvider provideFixPhp82Cases
+     *
+     * @requires PHP 8.2
+     */
+    public function testFixPhp82(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array<int, string>>
+     */
+    public static function provideFixPhp82Cases(): iterable
+    {
+        yield [
+            '<?php class Foo { public (A&B)|C $bar; }',
+            '<?php class Foo { public (A&B)|C$bar; }',
+        ];
+
+        yield [
+            '<?php class Foo { public (A&B)|C $bar; }',
+            '<?php class Foo { public (A&B)|C    $bar; }',
+        ];
+    }
+}