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

Issue:
`fully_qualified_strict_types` must fix attributes
## Feature request

Expected fixing behaviour:
```diff
-#[\Attr]
+#[Attr]
 class Cl {}
```
Support typed properties in `fully_qualified_strict_types`
Currently `fully_qualified_strict_types` can handle multiple scenarios, but does not cover class' properties. We need this fixer to make changes like this:

```diff
 use Foo\Bar;
 
 class SomeClass
 {
-    private \Foo\Bar $baz;
+    private Bar $baz;
 }
```

Pull request description:
feat: Support typed properties and attributes in `fully_qualified_strict_types`
fix #5004
fix #7657

Patch:
diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
index 86e92d735..fcdcb0e9a 100644
--- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
+++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
@@ -165,6 +165,7 @@ public function isCandidate(Tokens $tokens): bool
     {
         return $tokens->isAnyTokenKindsFound([
             CT::T_USE_TRAIT,
+            ...(\defined('T_ATTRIBUTE') ? [T_ATTRIBUTE] : []), // @TODO: drop condition when PHP 8.0+ is required
             T_CATCH,
             T_DOUBLE_COLON,
             T_DOC_COMMENT,
@@ -173,6 +174,7 @@ public function isCandidate(Tokens $tokens): bool
             T_IMPLEMENTS,
             T_INSTANCEOF,
             T_NEW,
+            T_VARIABLE,
         ]);
     }
 
@@ -250,6 +252,13 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                     $this->fixPrevName($tokens, $index, $uses, $namespaceName);
                 } elseif ($tokens[$index]->isGivenKind([T_INSTANCEOF, T_NEW, CT::T_USE_TRAIT])) {
                     $this->fixNextName($tokens, $index, $uses, $namespaceName);
+                } elseif ($tokens[$index]->isGivenKind(T_VARIABLE)) {
+                    $prevIndex = $tokens->getPrevMeaningfulToken($index);
+                    if (null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(T_STRING)) {
+                        $this->fixPrevName($tokens, $index, $uses, $namespaceName);
+                    }
+                } elseif (\defined('T_ATTRIBUTE') && $tokens[$index]->isGivenKind(T_ATTRIBUTE)) { // @TODO: drop const check when PHP 8.0+ is required
+                    $this->fixNextName($tokens, $index, $uses, $namespaceName);
                 }
 
                 if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
@@ -488,6 +497,17 @@ private function shortenClassIfPossible(Tokens $tokens, array $class, array &$us
             ) {
                 // if the type starts with namespace and the type is not the same as the namespace it can be shortened
                 $typeNameShort = substr($typeName, \strlen($namespaceName) + 1);
+
+                // if short names are the same, but long one are different then it cannot be shortened
+                foreach ($uses as $useLongName => $useShortName) {
+                    if (
+                        strtolower($typeNameShort) === strtolower($useShortName)
+                        && strtolower($typeName) !== strtolower($useLongName)
+                    ) {
+                        return;
+                    }
+                }
+
                 $this->replaceClassWithShort($tokens, $class, $typeNameShort);
             }
         }


Test patch:
diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
index d7efa6048..73bbd0f1c 100644
--- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
+++ b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
@@ -353,6 +353,21 @@ class SomeClass extends \Foo\Bar\A implements \Foo\Bar\Izumi, A, \A\B, \Foo\Bar\
             '<?php use A\B; class Foo { use \A\C; use \D; use \A\B { \A\B::bar as baz; } };',
         ];
 
+        yield 'typed property in class' => [
+            '<?php use A\B; class Cl { public B $p; var B $p2; }',
+            '<?php use A\B; class Cl { public \A\B $p; var \A\B $p2; }',
+        ];
+
+        yield 'typed property in anonymous class' => [
+            '<?php use A\B; new class() { public B $p; };',
+            '<?php use A\B; new class() { public \A\B $p; };',
+        ];
+
+        yield 'typed nullable property in class' => [
+            '<?php use A\B; class Cl { public ?B $p = null, $r; }',
+            '<?php use A\B; class Cl { public ?\A\B $p = null, $r; }',
+        ];
+
         yield 'starts with but not full name extends' => [
             '<?php namespace a\abcd;
 class Foo extends \a\abcdTest { }',
@@ -1754,6 +1769,59 @@ public static function provideFix80Cases(): iterable
             '<?php use A\B; try{ foo(0); } catch (B) {}',
             '<?php use A\B; try{ foo(0); } catch (\A\B) {}',
         ];
+
+        yield 'typed promoted property in class' => [
+            '<?php use A\B; class Cl { public function __construct(private B $p2) {} }',
+            '<?php use A\B; class Cl { public function __construct(private \A\B $p2) {} }',
+        ];
+
+        yield 'import new symbols from attributes' => [
+            '<?php
+
+namespace Foo\Test;
+use Other\ClassAttr;
+use Other\MethodAttr;
+use Other\PromotedAttr;
+use Other\PropertyAttr;
+
+#[ClassAttr]
+#[\AllowDynamicProperties]
+class Foo
+{
+    #[PropertyAttr]
+    public int $prop;
+
+    public function __construct(
+        #[PromotedAttr]
+        public int $arg
+    ) {}
+
+    #[MethodAttr]
+    public function foo(): void {}
+}
+            ',
+            '<?php
+
+namespace Foo\Test;
+
+#[\Other\ClassAttr]
+#[\AllowDynamicProperties]
+class Foo
+{
+    #[\Other\PropertyAttr]
+    public int $prop;
+
+    public function __construct(
+        #[\Other\PromotedAttr]
+        public int $arg
+    ) {}
+
+    #[\Other\MethodAttr]
+    public function foo(): void {}
+}
+            ',
+            ['import_symbols' => true],
+        ];
     }
 
     /**
diff --git a/tests/Fixtures/Integration/misc/PHP8_0.test b/tests/Fixtures/Integration/misc/PHP8_0.test
index 3e0734547..0c65152ad 100644
--- a/tests/Fixtures/Integration/misc/PHP8_0.test
+++ b/tests/Fixtures/Integration/misc/PHP8_0.test
@@ -15,6 +15,8 @@ PHP 8.0 test.
 
 declare(strict_types=1);
 
+use MyExample\MyAttribute2;
+
 // https://wiki.php.net/rfc/named_params
 array_fill(start_index: 0, num: 100, value: 50);
 htmlspecialchars($string, double_encode: false);
@@ -30,7 +32,7 @@ $country = $session?->user?->getAddress()?->country;
 // https://wiki.php.net/rfc/shorter_attribute_syntax
 // https://wiki.php.net/rfc/shorter_attribute_syntax_change
 #[MyAttribute]
-#[\MyExample\MyAttribute]
+#[MyAttribute2]
 #[MyAttribute(1234)]
 #[MyAttribute(value: 1234)]
 #[MyAttribute(MyAttribute::VALUE)]
@@ -190,7 +192,7 @@ $country = $session?->user?->getAddress()?->country;
 // https://wiki.php.net/rfc/shorter_attribute_syntax
 // https://wiki.php.net/rfc/shorter_attribute_syntax_change
 #[MyAttribute]
-#[\MyExample\MyAttribute]
+#[\MyExample\MyAttribute2]
 #[MyAttribute(1234)]
 #[MyAttribute(value: 1234)]
 #[MyAttribute(MyAttribute::VALUE)]