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

Issue:
 Two passes needed for no_superfluous_phpdoc_tags + phpdoc_add_missing_param_annotation
## Bug report

### Description

Two passes may be required if we use both `NoSuperfluousPhpdocTagsFixer` and `PhpdocAddMissingParamAnnotationFixer`.

### Runtime version

```
PHP CS Fixer 3.26.1 Crank Cake by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 7.4.33
```

### Configuration file

```php
<?php

$config = new PhpCsFixer\Config();

return $config
    ->setRules([
        'phpdoc_add_missing_param_annotation' => true
        'no_superfluous_phpdoc_tags' => true,
    ])
    ->setFinder(PhpCsFixer\Finder::create()
        ->in(__DIR__)
    )
;
```

### Code snippet that reproduces the problem

Let's assume we have this file:

```php
<?php

/**
 * @param int $bar Bar description
 * @param resource|null $baz=null Wrong baz description
 */
function foo($bar, $baz = null)
{
}
```

At the first pass, `NoSuperfluousPhpdocTagsFixer` changes it to

```php
<?php

/**
 * @param int $bar Bar description
 */
function foo($bar, $baz = null)
{
}
```

At the second pass, `PhpdocAddMissingParamAnnotationFixer` changes it to

```php
<?php

/**
 * @param int $bar Bar description
 * @param null|mixed $baz
 */
function foo($bar, $baz = null)
{
}
```

Pull request description:
fix: Look up for PHPDoc's variable name by only chars allowed in the variables
Fixes #7294 not by enforcing proper fixers' priorities, but by providing fix for how `@param`'s variable name is determined. PHPDoc params with descriptions are not considered as superfluous, but in order to do that, we need to determine variable name properly. Since `=` is not a char allowed in the variable name, it shouldn't be taken into consideration as a part of variable name when PHPDoc's line is parsed.

Patch:
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index cc9ae3f00..82d547eb5 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -55,6 +55,6 @@ parameters:
         -
             message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
             path: tests
-            count: 1013
+            count: 1012
     tipsOfTheDay: false
     tmpDir: dev-tools/phpstan/cache
diff --git a/src/DocBlock/Annotation.php b/src/DocBlock/Annotation.php
index 7c331a525..9fb304d96 100644
--- a/src/DocBlock/Annotation.php
+++ b/src/DocBlock/Annotation.php
@@ -171,14 +171,17 @@ public function getTypeExpression(): ?TypeExpression
     }
 
     /**
-     * @return null|string
-     *
      * @internal
      */
-    public function getVariableName()
+    public function getVariableName(): ?string
     {
         $type = preg_quote($this->getTypesContent() ?? '', '/');
-        $regex = "/@{$this->tag->getName()}\\s+({$type}\\s*)?(&\\s*)?(\\.{3}\\s*)?(?<variable>\\$.+?)(?:[\\s*]|$)/";
+        $regex = sprintf(
+            '/@%s\s+(%s\s*)?(&\s*)?(\.{3}\s*)?(?<variable>\$%s)(?:.*|$)/',
+            $this->tag->getName(),
+            $type,
+            TypeExpression::REGEX_IDENTIFIER
+        );
 
         if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
             return $matches['variable'];


Test patch:
diff --git a/tests/DocBlock/AnnotationTest.php b/tests/DocBlock/AnnotationTest.php
index 4e7cb488b..203245ab5 100644
--- a/tests/DocBlock/AnnotationTest.php
+++ b/tests/DocBlock/AnnotationTest.php
@@ -637,6 +637,9 @@ public function testGetVariableName(string $line, ?string $expectedVariableName)
         self::assertSame($expectedVariableName, $annotation->getVariableName());
     }
 
+    /**
+     * @return iterable<array{string, null|string}>
+     */
     public static function provideGetVariableNameCases(): iterable
     {
         yield ['* @param int $foo', '$foo'];
@@ -676,5 +679,9 @@ public static function provideGetVariableNameCases(): iterable
         yield ['* @param int & ...$foo', '$foo'];
 
         yield ['* @param int & ... $foo', '$foo'];
+
+        yield ['* @param ?int $foo=null invalid description', '$foo'];
+
+        yield ['* @param int $počet Special chars in variable name', '$počet'];
     }
 }
diff --git a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
index fecf7abfb..b0bb82494 100644
--- a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
+++ b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
@@ -2608,6 +2608,17 @@ function foo(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $co
                 EOD,
             ['allow_hidden_params' => true],
         ];
+
+        yield '@param without space between variable name and description' => [
+            <<<'EOD'
+                <?php
+                /**
+                 * @param int $bar Bar description
+                 * @param resource|null $baz=null Wrong baz description
+                 */
+                function foo($bar, $baz = null) {}
+                EOD,
+        ];
     }
 
     /**