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

Issue:
Broken behaviour of NullableTypeDeclarationForDefaultNullValueFixer with php8 constructor property promotion
## Bug report

```bash
$ php -v
PHP 8.0.0 (cli) (built: Dec 17 2020 08:54:56) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
    with Xdebug v3.0.2, Copyright (c) 2002-2021, by Derick Rethans
    with Zend OPcache v8.0.0, Copyright (c), by Zend Technologies
```

```bash
$ composer show | grep friendsofphp/php-cs-fixer
friendsofphp/php-cs-fixer            v2.18.0            A tool to automatically fix PHP code style
```

```diff
1) app/Security/User/EmailPasswordUser.php

    ---------- begin diff ----------
--- Original
+++ New
@@ -19,13 +19,12 @@
      * @param string[] $roles
      */
     public function __construct(
-        private ?UuidInterface $userProfileId = null,
+        ?private ?UuidInterface $userProfileId = null,
         private string $username = '',
         private string $password = '',
-        private ?string $salt = null,
+        ?private ?string $salt = null,
         private array $roles = []
-    )
-    {
+    ) {
     }

     public static function fromUserProfileAndAuthInfo(UserProfile $userProfile, EmailPasswordAuthInfo $authInfo): self
    ----------- end diff -----------


Applied checkers:

 * PhpCsFixer\Fixer\Basic\BracesFixer
 * PhpCsFixer\Fixer\FunctionNotation\NullableTypeDeclarationForDefaultNullValueFixer
```

### Code snippet that reproduces the problem

```php
<?php

declare(strict_types=1);

use PhpCsFixer\Fixer\FunctionNotation\FunctionDeclarationFixer;
use PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\EasyCodingStandard\ValueObject\Option;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;

return static function (ContainerConfigurator $containerConfigurator): void {
    // $services = $containerConfigurator->services();

    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::PATHS, [__DIR__.'/src', __DIR__.'/app', __DIR__.'/config', __DIR__.'/ecs.php']);

    $parameters->set(
        Option::SETS,
        [
            SetList::PHP_71,
        ]
    );

    $dirs = [__DIR__.'/src/**', __DIR__.'/app/**'];

    $parameters->set(
        Option::SKIP,
        [
            NotOperatorWithSuccessorSpaceFixer::class => $dirs,
            FunctionDeclarationFixer::class => $dirs,
        ]
    );
};

```

```bash
php vendor/bin/ecs check app/Security/User/EmailPasswordUser.php
```


Pull request description:
NullableTypeDeclarationForDefaultNullValueFixer - support property promotion via constructor
closes #5450 

Patch:
diff --git a/src/Tokenizer/Analyzer/ArgumentsAnalyzer.php b/src/Tokenizer/Analyzer/ArgumentsAnalyzer.php
index 0904a04fd..67a68d35d 100644
--- a/src/Tokenizer/Analyzer/ArgumentsAnalyzer.php
+++ b/src/Tokenizer/Analyzer/ArgumentsAnalyzer.php
@@ -14,6 +14,7 @@
 
 use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
 use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
+use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Tokens;
 
 /**
@@ -112,7 +113,12 @@ public function getArgumentInfo(Tokens $tokens, $argumentStart, $argumentEnd)
         for ($index = $argumentStart; $index <= $argumentEnd; ++$index) {
             $token = $tokens[$index];
 
-            if ($token->isComment() || $token->isWhitespace() || $token->isGivenKind(T_ELLIPSIS) || $token->equals('&')) {
+            if (
+                $token->isComment()
+                || $token->isWhitespace()
+                || $token->isGivenKind([T_ELLIPSIS, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE])
+                || $token->equals('&')
+            ) {
                 continue;
             }
 


Test patch:
diff --git a/tests/Fixer/FunctionNotation/NullableTypeDeclarationForDefaultNullValueFixerTest.php b/tests/Fixer/FunctionNotation/NullableTypeDeclarationForDefaultNullValueFixerTest.php
index 581effe85..b94743d90 100644
--- a/tests/Fixer/FunctionNotation/NullableTypeDeclarationForDefaultNullValueFixerTest.php
+++ b/tests/Fixer/FunctionNotation/NullableTypeDeclarationForDefaultNullValueFixerTest.php
@@ -404,13 +404,13 @@ public function provideFixPhp74Cases()
     }
 
     /**
-     * @param string $expected
-     * @param string $input
+     * @param string      $expected
+     * @param null|string $input
      *
      * @dataProvider provideFix80Cases
      * @requires PHP 8.0
      */
-    public function testFix80($expected, $input)
+    public function testFix80($expected, $input = null)
     {
         $this->doTest($expected, $input);
     }
@@ -421,5 +421,17 @@ public function provideFix80Cases()
             '<?php function foo(?string $param = null,) {}',
             '<?php function foo(string $param = null,) {}',
         ];
+
+        yield [
+            '<?php class Foo {
+                public function __construct(
+                    public ?string $paramA = null,
+                    protected ?string $paramB = null,
+                    private ?string $paramC = null,
+                    ?string $paramD = null,
+                    $a = []
+                ) {}
+            }',
+        ];
     }
 }
diff --git a/tests/Tokenizer/Analyzer/ArgumentsAnalyzerTest.php b/tests/Tokenizer/Analyzer/ArgumentsAnalyzerTest.php
index 8b6ab1262..96c811a03 100644
--- a/tests/Tokenizer/Analyzer/ArgumentsAnalyzerTest.php
+++ b/tests/Tokenizer/Analyzer/ArgumentsAnalyzerTest.php
@@ -62,6 +62,26 @@ public function testArgumentInfo($code, $openIndex, $closeIndex, $expected)
         );
     }
 
+    /**
+     * @param string $code
+     * @param int    $openIndex
+     * @param int    $closeIndex
+     * @param array  $expected
+     *
+     * @requires PHP 8.0
+     * @dataProvider provideArgumentsInfo80Cases
+     */
+    public function testArgumentInfo80($code, $openIndex, $closeIndex, $expected)
+    {
+        $tokens = Tokens::fromCode($code);
+        $analyzer = new ArgumentsAnalyzer();
+
+        static::assertSame(
+            serialize($expected),
+            serialize($analyzer->getArgumentInfo($tokens, $openIndex, $closeIndex))
+        );
+    }
+
     public function provideArgumentsCases()
     {
         return [
@@ -162,6 +182,27 @@ public function provideArgumentsInfoCases()
         ];
     }
 
+    public function provideArgumentsInfo80Cases()
+    {
+        foreach (['public', 'protected', 'private'] as $visibility) {
+            yield [
+                sprintf('<?php class Foo { public function __construct(%s ?string $param = null) {} }', $visibility),
+                13,
+                22,
+                new ArgumentAnalysis(
+                    '$param',
+                    18,
+                    'null',
+                    new TypeAnalysis(
+                        '?string',
+                        15,
+                        16
+                    )
+                ),
+            ];
+        }
+    }
+
     /**
      * @param string $code
      * @param int    $openIndex
@@ -190,4 +231,30 @@ public function provideArguments73Cases()
             ['<?php bar($a, $b , ) ;', 2, 10, [3 => 3, 5 => 7]],
         ];
     }
+
+    /**
+     * @param string $code
+     * @param int    $openIndex
+     * @param int    $closeIndex
+     *
+     * @requires PHP 8.0
+     * @dataProvider provideArguments80Cases
+     */
+    public function testArguments80($code, $openIndex, $closeIndex, array $arguments)
+    {
+        $tokens = Tokens::fromCode($code);
+        $analyzer = new ArgumentsAnalyzer();
+
+        static::assertSame(\count($arguments), $analyzer->countArguments($tokens, $openIndex, $closeIndex));
+        static::assertSame($arguments, $analyzer->getArguments($tokens, $openIndex, $closeIndex));
+    }
+
+    public function provideArguments80Cases()
+    {
+        return [
+            ['<?php class Foo { public function __construct(public ?string $param = null) {} }', 12, 23, [13 => 22]],
+            ['<?php class Foo { public function __construct(protected ?string $param = null) {} }', 12, 23, [13 => 22]],
+            ['<?php class Foo { public function __construct(private ?string $param = null) {} }', 12, 23, [13 => 22]],
+        ];
+    }
 }