diff --git a/doc/rules/import/fully_qualified_strict_types.rst b/doc/rules/import/fully_qualified_strict_types.rst index 12ab3d80ee4..56d2f80b6a0 100644 --- a/doc/rules/import/fully_qualified_strict_types.rst +++ b/doc/rules/import/fully_qualified_strict_types.rst @@ -35,16 +35,32 @@ Example #1 use Foo\Bar; use Foo\Bar\Baz; + /** + - * @see \Foo\Bar\Baz + + * @see Baz + */ class SomeClass { - - public function doX(\Foo\Bar $foo): \Foo\Bar\Baz - + public function doX(Bar $foo): Baz - { + /** + - * @var \Foo\Bar\Baz + + * @var Baz + */ + public $baz; + + /** + - * @param \Foo\Bar\Baz $baz + + * @param Baz $baz + */ + public function __construct($baz) { + $this->baz = $baz; } - - public function doY(Foo\NotImported $u, \Foo\NotImported $v) - + public function doY(Foo\NotImported $u, Foo\NotImported $v) - { + /** + - * @return \Foo\Bar\Baz + + * @return Baz + */ + public function getBaz() { + return $this->baz; } } diff --git a/phpstan.dist.neon b/phpstan.dist.neon index bc0d0554113..15cd9324da3 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -21,7 +21,7 @@ parameters: - message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#' path: tests - count: 1120 + count: 1113 - message: '#Call to static method .+ with .+ will always evaluate to true.$#' diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php index e9f04f3c976..5589eea1a47 100644 --- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php +++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php @@ -22,6 +22,7 @@ use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; +use PhpCsFixer\Preg; use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis; use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer; use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; @@ -31,6 +32,8 @@ /** * @author VeeWee + * @author Tomas Jadrny + * @author Greg Korba */ final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface { @@ -45,14 +48,28 @@ public function getDefinition(): FixerDefinitionInterface use Foo\Bar; use Foo\Bar\Baz; +/** + * @see \Foo\Bar\Baz + */ class SomeClass { - public function doX(\Foo\Bar $foo): \Foo\Bar\Baz - { + /** + * @var \Foo\Bar\Baz + */ + public $baz; + + /** + * @param \Foo\Bar\Baz $baz + */ + public function __construct($baz) { + $this->baz = $baz; } - public function doY(Foo\NotImported $u, \Foo\NotImported $v) - { + /** + * @return \Foo\Bar\Baz + */ + public function getBaz() { + return $this->baz; } } ' @@ -86,7 +103,7 @@ public function getPriority(): int public function isCandidate(Tokens $tokens): bool { - return $tokens->isTokenKindFound(T_FUNCTION); + return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_DOC_COMMENT]); } protected function createConfigurationDefinition(): FixerConfigurationResolverInterface @@ -119,6 +136,10 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void if ($tokens[$index]->isGivenKind(T_FUNCTION)) { $this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName); } + + if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) { + $this->fixPhpDoc($tokens, $index, $uses, $namespaceName); + } } } } @@ -145,6 +166,40 @@ private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $token } } + /** + * @param array $uses + */ + private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $namespaceName): void + { + $phpDoc = $tokens[$index]; + $phpDocContent = $phpDoc->getContent(); + Preg::matchAll('#@([^\s]+)\s+([^\s]+)#', $phpDocContent, $matches); + + if ([] !== $matches) { + foreach ($matches[2] as $i => $typeName) { + if (!\in_array($matches[1][$i], ['param', 'return', 'see', 'throws', 'var'], true)) { + continue; + } + + $shortTokens = $this->determineShortType($typeName, $uses, $namespaceName); + + if (null !== $shortTokens) { + // Replace tag+type in order to avoid replacing type multiple times (when same type is used in multiple places) + $phpDocContent = str_replace( + $matches[0][$i], + '@'.$matches[1][$i].' '.implode('', array_map( + static fn (Token $token) => $token->getContent(), + $shortTokens + )), + $phpDocContent + ); + } + } + + $tokens[$index] = new Token([T_DOC_COMMENT, $phpDocContent]); + } + } + /** * @param array $uses */ @@ -156,7 +211,6 @@ private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $u $typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex); } - $namespaceNameLength = \strlen($namespaceName); $types = $this->getTypes($tokens, $typeStartIndex, $type->getEndIndex()); foreach ($types as $typeName => [$startIndex, $endIndex]) { @@ -164,60 +218,76 @@ private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $u return; } - $withLeadingBackslash = str_starts_with($typeName, '\\'); - if ($withLeadingBackslash) { - $typeName = substr($typeName, 1); + $shortType = $this->determineShortType($typeName, $uses, $namespaceName); + + if (null !== $shortType) { + $tokens->overrideRange($startIndex, $endIndex, $shortType); } - $typeNameLower = strtolower($typeName); + } + } - if (isset($uses[$typeNameLower]) && ($withLeadingBackslash || '' === $namespaceName)) { - // if the type without leading "\" equals any of the full "uses" long names, it can be replaced with the short one - $tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($uses[$typeNameLower])); + /** + * Determines short type based on FQCN, current namespace and imports (`use` declarations). + * + * @param array $uses + * + * @return null|Token[] + */ + private function determineShortType(string $typeName, array $uses, string $namespaceName): ?array + { + $withLeadingBackslash = str_starts_with($typeName, '\\'); + if ($withLeadingBackslash) { + $typeName = substr($typeName, 1); + } + $typeNameLower = strtolower($typeName); + $namespaceNameLength = \strlen($namespaceName); - continue; - } + if (isset($uses[$typeNameLower]) && ($withLeadingBackslash || '' === $namespaceName)) { + // if the type without leading "\" equals any of the full "uses" long names, it can be replaced with the short one + return $this->namespacedStringToTokens($uses[$typeNameLower]); + } - if ('' === $namespaceName) { - foreach ($uses as $useShortName) { - if (strtolower($useShortName) === $typeNameLower) { - continue 2; - } + if ('' === $namespaceName) { + // if we are in the global namespace and the type is not imported the leading '\' can be removed (TODO nice config candidate) + foreach ($uses as $useShortName) { + if (strtolower($useShortName) === $typeNameLower) { + return null; } + } - // if we are in the global namespace and the type is not imported, - // we enforce/remove leading backslash (depending on the configuration) - if (true === $this->configuration['leading_backslash_in_global_namespace']) { - if (!$withLeadingBackslash && !isset($uses[$typeNameLower])) { - $tokens->overrideRange( - $startIndex, - $endIndex, - $this->namespacedStringToTokens($typeName, true) - ); - } - } else { - $tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeName)); + // if we are in the global namespace and the type is not imported, + // we enforce/remove leading backslash (depending on the configuration) + if (true === $this->configuration['leading_backslash_in_global_namespace']) { + if (!$withLeadingBackslash && !isset($uses[$typeNameLower])) { + return $this->namespacedStringToTokens($typeName, true); } - } elseif (!str_contains($typeName, '\\')) { - // If we're NOT in the global namespace, there's no related import, - // AND used type is from global namespace, then it can't be shortened. - continue; - } elseif ($typeNameLower !== $namespaceName && str_starts_with($typeNameLower, $namespaceName.'\\')) { - // if the type starts with namespace and the type is not the same as the namespace it can be shortened - $typeNameShort = substr($typeName, $namespaceNameLength + 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) - ) { - continue 2; - } + } else { + return $this->namespacedStringToTokens($typeName); + } + } + if (!str_contains($typeName, '\\')) { + // If we're NOT in the global namespace, there's no related import, + // AND used type is from global namespace, then it can't be shortened. + return null; + } + if ($typeNameLower !== $namespaceName && str_starts_with($typeNameLower, $namespaceName.'\\')) { + // if the type starts with namespace and the type is not the same as the namespace it can be shortened + $typeNameShort = substr($typeName, $namespaceNameLength + 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 null; } - - $tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeNameShort)); } + + return $this->namespacedStringToTokens($typeNameShort); } + + return null; } /** diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php index 82d497c6dc6..33dbcc00b8e 100644 --- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php +++ b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php @@ -36,6 +36,9 @@ public function testNewLogic(string $expected, ?string $input = null, array $con $this->doTest($expected, $input); } + /** + * @return iterable}> + */ public static function provideNewLogicCases(): iterable { yield 'namespace === type name' => [ @@ -164,6 +167,9 @@ public function testCodeWithReturnTypes(string $expected, ?string $input = null) $this->doTest($expected, $input); } + /** + * @return iterable + */ public static function provideCodeWithReturnTypesCases(): iterable { yield 'Import common strict types' => [ @@ -376,6 +382,9 @@ public function testCodeWithoutReturnTypes(string $expected, ?string $input = nu $this->doTest($expected, $input); } + /** + * @return iterable + */ public static function provideCodeWithoutReturnTypesCases(): iterable { yield 'import from namespace and global' => [ @@ -635,6 +644,9 @@ public function __construct( ]; } + /** + * @return iterable + */ public static function provideCodeWithReturnTypesCasesWithNullableCases(): iterable { yield 'Test namespace fixes with nullable types' => [ @@ -687,6 +699,359 @@ public function three(Three\Four $four): ?Two\Three ]; } + /** + * @param array $config + * + * @dataProvider provideCodeWithPhpDocCases + */ + public function testCodeWithPhpDoc(string $expected, ?string $input = null, array $config = []): void + { + $this->fixer->configure($config); + $this->doTest($expected, $input); + } + + /** + * @return iterable}> + */ + public static function provideCodeWithPhpDocCases(): iterable + { + yield 'Test class PHPDoc fixes' => [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + 'baz = $baz; + $this->bam = $bam; + } + + /** + * @return Buzz + */ + public function getBaz() { + return $this->baz; + } + + /** + * @return Boom + */ + public function getBam() { + return $this->bam; + } +}', + 'baz = $baz; + $this->bam = $bam; + } + + /** + * @return \Foo\Bar\Baz + */ + public function getBaz() { + return $this->baz; + } + + /** + * @return \Foo\Bar\Bam + */ + public function getBam() { + return $this->bam; + } +}', + ]; + + yield 'Leading backslash in global namespace' => [ + ' true], + ]; + } + /** * @requires PHP 8.0 * @@ -697,6 +1062,9 @@ public function testFix80(string $expected, ?string $input = null): void $this->doTest($expected, $input); } + /** + * @return iterable + */ public static function provideFix80Cases(): iterable { yield [ @@ -741,6 +1109,9 @@ public function testFix81(string $expected, ?string $input = null, array $config $this->doTest($expected, $input); } + /** + * @return iterable}> + */ public static function provideFix81Cases(): iterable { yield [ @@ -795,6 +1166,9 @@ public function testFix82(string $expected, ?string $input = null, array $config $this->doTest($expected, $input); } + /** + * @return iterable}> + */ public static function provideFix82Cases(): iterable { yield 'simple param in global namespace without use' => [ diff --git a/tests/Fixtures/Integration/priority/phpdoc_to_return_type,fully_qualified_strict_types.test b/tests/Fixtures/Integration/priority/phpdoc_to_return_type,fully_qualified_strict_types.test index 854b9f67f27..8f82fb732bf 100644 --- a/tests/Fixtures/Integration/priority/phpdoc_to_return_type,fully_qualified_strict_types.test +++ b/tests/Fixtures/Integration/priority/phpdoc_to_return_type,fully_qualified_strict_types.test @@ -5,7 +5,7 @@ Integration of fixers: phpdoc_to_return_type,fully_qualified_strict_types. --EXPECT--