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

Issue:
`fully_qualified_strict_types` breaks @see tag
## Bug report

<!--
  Before reporting a bug, make sure PHP CS Fixer is up-to-date.

  Check that the issue is NOT regarded to an applied rule by using the `-vvv` option (will list applied rules) on a minimal code snippet.
-->

### Description

`fully_qualified_strict_types` breaks @see tag by AddSeeTestAnnotationRector
https://github.com/rectorphp/rector-phpunit/blob/main/docs/rector_rules_overview.md#addseetestannotationrector

```diff
- * @see \CodeIgniter\Pager\PagerRendererTest
+ * @see PagerRendererTest
```

See also https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc-tags.md#513-see

### Runtime version

PHP CS Fixer 3.42.0 Three Keys by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.2.13

### Used command

<!--
  Provide the command used to run PHP CS Fixer (run with `-vvv`).
-->
php-cs-fixer fix --ansi --verbose --diff

### Configuration file

<!--
  Provide content of configuration file you used (.php-cs-fixer[.dist].php).
-->

### Code snippet that reproduces the problem

<!--
  Provide a minimal code snippet before running PHP CS Fixer. If relevant,
  also provide the expected output, the actual output and/or the error that
  occurred, if any.

  Please use markdown syntax for each code snippet, e.g.:

  ```php
  <?php
  $var = 'foo';
  ```

-->


Pull request:
feat: Configurable phpDoc tags for FQCN processing
Fixes #7620 by providing ability to choose which phpDoc tags should be processed (in that case `@see` can be omitted and  than problem is solved). I chose the approach with configurable list of phpDoc tags because it's more flexible - not only it allows disabling phpDoc support completely, but also allows narrowing/widening default scope by providing custom list of tags.

Patch:
diff --git a/doc/rules/import/fully_qualified_strict_types.rst b/doc/rules/import/fully_qualified_strict_types.rst
index ed9f59a61..02f9459c9 100644
--- a/doc/rules/import/fully_qualified_strict_types.rst
+++ b/doc/rules/import/fully_qualified_strict_types.rst
@@ -29,6 +29,16 @@ Allowed types: ``bool``
 
 Default value: ``false``
 
+``phpdoc_tags``
+~~~~~~~~~~~~~~~
+
+Collection of PHPDoc annotation tags where FQCNs should be processed. As of now
+only simple tags with ``@tag \F\Q\C\N`` format are supported (no complex types).
+
+Allowed types: ``array``
+
+Default value: ``['param', 'phpstan-param', 'phpstan-property', 'phpstan-property-read', 'phpstan-property-write', 'phpstan-return', 'phpstan-var', 'property', 'property-read', 'property-write', 'psalm-param', 'psalm-property', 'psalm-property-read', 'psalm-property-write', 'psalm-return', 'psalm-var', 'return', 'see', 'throws', 'var']``
+
 Examples
 --------
 
diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
index b1822cae1..60e713237 100644
--- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
+++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
@@ -188,6 +188,34 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
                 ->setAllowedTypes(['bool'])
                 ->setDefault(false)
                 ->getOption(),
+            (new FixerOptionBuilder(
+                'phpdoc_tags',
+                'Collection of PHPDoc annotation tags where FQCNs should be processed. As of now only simple tags with `@tag \F\Q\C\N` format are supported (no complex types).'
+            ))
+                ->setAllowedTypes(['array'])
+                ->setDefault([
+                    'param',
+                    'phpstan-param',
+                    'phpstan-property',
+                    'phpstan-property-read',
+                    'phpstan-property-write',
+                    'phpstan-return',
+                    'phpstan-var',
+                    'property',
+                    'property-read',
+                    'property-write',
+                    'psalm-param',
+                    'psalm-property',
+                    'psalm-property-read',
+                    'psalm-property-write',
+                    'psalm-return',
+                    'psalm-var',
+                    'return',
+                    'see',
+                    'throws',
+                    'var',
+                ])
+                ->getOption(),
         ]);
     }
 
@@ -260,13 +288,19 @@ private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $token
      */
     private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $namespaceName): void
     {
+        $allowedTags = $this->configuration['phpdoc_tags'];
+
+        if ([] === $allowedTags) {
+            return;
+        }
+
         $phpDoc = $tokens[$index];
         $phpDocContent = $phpDoc->getContent();
         Preg::matchAll('#@([^\s]+)(\s+)([a-zA-Z0-9_\\\\]+)#', $phpDocContent, $matches);
 
         if ([] !== $matches[0]) {
             foreach ($matches[3] as $i => $typeName) {
-                if (!\in_array($matches[1][$i], ['param', 'return', 'see', 'throws', 'var'], true)) {
+                if (!\in_array($matches[1][$i], $allowedTags, true)) {
                     continue;
                 }
 


Test patch:
diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
index 6d61d7102..12b81a773 100644
--- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
+++ b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
@@ -1095,7 +1095,7 @@ public function testCodeWithPhpDoc(string $expected, ?string $input = null, arra
     }
 
     /**
-     * @return iterable<string, array{0: string, 1?: string, 2?: array<string, mixed>}>
+     * @return iterable<string, array{0: string, 1?: null|string, 2?: array<string, mixed>}>
      */
     public static function provideCodeWithPhpDocCases(): iterable
     {
@@ -1274,14 +1274,24 @@ final class SomeClass {}',
 /**
  * @see Baz
  * @see Bam
+ *
+ * @property SomeClass $foo
+ * @property-read Buz $buz
+ * @property-write Baz $baz
+ * @phpstan-property SomeClass $foo
+ * @phpstan-property-read Buz $buz
+ * @phpstan-property-write Baz $baz
+ * @psalm-property SomeClass $foo
+ * @psalm-property-read Buz $buz
+ * @psalm-property-write Baz $baz
  */
 interface SomeClass
 {
     /**
     * @param SomeClass $foo
-    * @param Buz $buz
+    * @phpstan-param Buz $buz
     *
-    * @return Baz
+    * @psalm-return Baz
     */
     public function doSomething(SomeClass $foo, Buz $buz): Baz;
 }',
@@ -1296,14 +1306,24 @@ public function doSomething(SomeClass $foo, Buz $buz): Baz;
 /**
  * @see \Foo\Bar\Baz
  * @see \Foo\Bar\Bam
+ *
+ * @property \Foo\Bar\SomeClass $foo
+ * @property-read \Foo\Bar\Buz $buz
+ * @property-write \Foo\Bar\Baz $baz
+ * @phpstan-property \Foo\Bar\SomeClass $foo
+ * @phpstan-property-read \Foo\Bar\Buz $buz
+ * @phpstan-property-write \Foo\Bar\Baz $baz
+ * @psalm-property \Foo\Bar\SomeClass $foo
+ * @psalm-property-read \Foo\Bar\Buz $buz
+ * @psalm-property-write \Foo\Bar\Baz $baz
  */
 interface SomeClass
 {
     /**
     * @param \Foo\Bar\SomeClass $foo
-    * @param \Foo\Bar\Buz $buz
+    * @phpstan-param \Foo\Bar\Buz $buz
     *
-    * @return \Foo\Bar\Baz
+    * @psalm-return \Foo\Bar\Baz
     */
     public function doSomething(\Foo\Bar\SomeClass $foo, \Foo\Bar\Buz $buz): \Foo\Bar\Baz;
 }',
@@ -1435,6 +1455,85 @@ function foo($dateTime) {}',
             ['leading_backslash_in_global_namespace' => true],
         ];
 
+        yield 'Do not touch PHPDoc if configured with empty collection' => [
+            '<?php
+
+namespace Foo\Bar;
+
+use Foo\Bar\Buz;
+use Foo\Bar\Baz;
+use Foo\Bar\SomeClass;
+
+/**
+ * @see \Foo\Bar\Baz
+ * @see \Foo\Bar\Bam
+ *
+ * @property \Foo\Bar\SomeClass $foo
+ * @property-read \Foo\Bar\Buz $buz
+ * @property-write \Foo\Bar\Baz $baz
+ */
+interface SomeClass
+{
+    /**
+    * @param \Foo\Bar\SomeClass $foo
+    * @phpstan-param \Foo\Bar\Buz $buz
+    *
+    * @psalm-return \Foo\Bar\Baz
+    */
+    public function doSomething($foo, $buz);
+}',
+            null,
+            ['phpdoc_tags' => []],
+        ];
+
+        yield 'Process only specified PHPDoc annotation' => [
+            '<?php
+
+namespace Foo\Bar;
+
+use Foo\Baz\Buzz;
+
+/**
+ * @see \Foo\Baz\Buzz
+ *
+ * @property \Foo\Baz\Buzz $buzz1
+ * @property-read Buzz $buzz2
+ */
+interface SomeClass
+{
+    /**
+    * @param \Foo\Baz\Buzz $a
+    * @phpstan-param Buzz $b
+    *
+    * @psalm-return \Foo\Baz\Buzz
+    */
+    public function doSomething($a, $b): void;
+}',
+            '<?php
+
+namespace Foo\Bar;
+
+use Foo\Baz\Buzz;
+
+/**
+ * @see \Foo\Baz\Buzz
+ *
+ * @property \Foo\Baz\Buzz $buzz1
+ * @property-read \Foo\Baz\Buzz $buzz2
+ */
+interface SomeClass
+{
+    /**
+    * @param \Foo\Baz\Buzz $a
+    * @phpstan-param \Foo\Baz\Buzz $b
+    *
+    * @psalm-return \Foo\Baz\Buzz
+    */
+    public function doSomething($a, $b): void;
+}',
+            ['phpdoc_tags' => ['property-read', 'phpstan-param']],
+        ];
+
         yield 'ignore @see with URL' => [
             '<?php
 /**