Skip to content

Commit

Permalink
feat: Support more FQCNs cases in fully_qualified_strict_types (PHP…
Browse files Browse the repository at this point in the history
…-CS-Fixer#7459)

Co-authored-by: SpacePossum <possumfromspace@gmail.com>
  • Loading branch information
Wirone and SpacePossum committed Dec 15, 2023
1 parent a850084 commit 1bc8e82
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 20 deletions.
45 changes: 42 additions & 3 deletions doc/rules/import/fully_qualified_strict_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Rule ``fully_qualified_strict_types``
=====================================

Transforms imported FQCN parameters and return types in function arguments to
short version.
Removes the leading part of fully qualified symbol references if a given symbol
is imported or belongs to the current namespace. Fixes function arguments,
exceptions in ``catch`` block, ``extend`` and ``implements`` of classes and
interfaces.

Configuration
-------------
Expand Down Expand Up @@ -34,12 +36,16 @@ Example #1
use Foo\Bar;
use Foo\Bar\Baz;
use Foo\OtherClass;
use Foo\SomeContract;
use Foo\SomeException;
/**
- * @see \Foo\Bar\Baz
+ * @see Baz
*/
class SomeClass
-class SomeClass extends \Foo\OtherClass implements \Foo\SomeContract
+class SomeClass extends OtherClass implements SomeContract
{
/**
- * @var \Foo\Bar\Baz
Expand All @@ -62,6 +68,14 @@ Example #1
public function getBaz() {
return $this->baz;
}
- public function doX(\Foo\Bar $foo, \Exception $e): \Foo\Bar\Baz
+ public function doX(Bar $foo, Exception $e): Baz
{
try {}
- catch (\Foo\SomeException $e) {}
+ catch (SomeException $e) {}
}
}
Example #2
Expand All @@ -83,6 +97,31 @@ With configuration: ``['leading_backslash_in_global_namespace' => true]``.
}
}
Example #3
~~~~~~~~~~

With configuration: ``['leading_backslash_in_global_namespace' => true]``.

.. code-block:: diff
--- Original
+++ New
<?php
namespace {
use Foo\A;
try {
foo();
- } catch (\Exception|\Foo\A $e) {
+ } catch (Exception|A $e) {
}
}
namespace Foo\Bar {
- class SomeClass implements \Foo\Bar\Baz
+ class SomeClass implements Baz
{
}
}
Rule sets
---------

Expand Down
2 changes: 1 addition & 1 deletion doc/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ Import

- `fully_qualified_strict_types <./import/fully_qualified_strict_types.rst>`_

Transforms imported FQCN parameters and return types in function arguments to short version.
Removes the leading part of fully qualified symbol references if a given symbol is imported or belongs to the current namespace. Fixes function arguments, exceptions in ``catch`` block, ``extend`` and ``implements`` of classes and interfaces.
- `global_namespace_import <./import/global_namespace_import.rst>`_

Imports or fully qualifies global classes/functions/constants.
Expand Down
188 changes: 185 additions & 3 deletions src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,28 @@
* @author VeeWee <toonverwerft@gmail.com>
* @author Tomas Jadrny <developer@tomasjadrny.cz>
* @author Greg Korba <greg@codito.dev>
* @author SpacePossum <possumfromspace@gmail.com>
*/
final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Transforms imported FQCN parameters and return types in function arguments to short version.',
'Removes the leading part of fully qualified symbol references if a given symbol is imported or belongs to the current namespace. Fixes function arguments, exceptions in `catch` block, `extend` and `implements` of classes and interfaces.',
[
new CodeSample(
'<?php
use Foo\Bar;
use Foo\Bar\Baz;
use Foo\OtherClass;
use Foo\SomeContract;
use Foo\SomeException;
/**
* @see \Foo\Bar\Baz
*/
class SomeClass
class SomeClass extends \Foo\OtherClass implements \Foo\SomeContract
{
/**
* @var \Foo\Bar\Baz
Expand All @@ -71,6 +75,12 @@ public function __construct($baz) {
public function getBaz() {
return $this->baz;
}
public function doX(\Foo\Bar $foo, \Exception $e): \Foo\Bar\Baz
{
try {}
catch (\Foo\SomeException $e) {}
}
}
'
),
Expand All @@ -83,6 +93,23 @@ public function doY(Foo\NotImported $u, \Foo\NotImported $v)
{
}
}
',
['leading_backslash_in_global_namespace' => true]
),
new CodeSample(
'<?php
namespace {
use Foo\A;
try {
foo();
} catch (\Exception|\Foo\A $e) {
}
}
namespace Foo\Bar {
class SomeClass implements \Foo\Bar\Baz
{
}
}
',
['leading_backslash_in_global_namespace' => true]
),
Expand All @@ -103,7 +130,14 @@ public function getPriority(): int

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_DOC_COMMENT]);
return $tokens->isAnyTokenKindsFound([
T_FUNCTION,
T_DOC_COMMENT,
T_IMPLEMENTS,
T_EXTENDS,
T_CATCH,
T_DOUBLE_COLON,
]);
}

protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
Expand Down Expand Up @@ -135,6 +169,12 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); ++$index) {
if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
$this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName);
} elseif ($tokens[$index]->isGivenKind([T_EXTENDS, T_IMPLEMENTS])) {
$this->fixExtendsImplements($tokens, $index, $uses, $namespaceName);
} elseif ($tokens[$index]->isGivenKind(T_CATCH)) {
$this->fixCatch($tokens, $index, $uses, $namespaceName);
} elseif ($tokens[$index]->isGivenKind(T_DOUBLE_COLON)) {
$this->fixClassStaticAccess($tokens, $index, $uses, $namespaceName);
}

if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
Expand Down Expand Up @@ -200,6 +240,130 @@ private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $name
}
}

/**
* @param array<string, string> $uses
*/
private function fixExtendsImplements(Tokens $tokens, int $index, array $uses, string $namespaceName): void
{
$index = $tokens->getNextMeaningfulToken($index);
$extend = ['content' => '', 'tokens' => []];

while (true) {
if ($tokens[$index]->equalsAny([',', '{', [T_IMPLEMENTS]])) {
$this->shortenClassIfPossible($tokens, $extend, $uses, $namespaceName);

if ($tokens[$index]->equals('{')) {
break;
}

$extend = ['content' => '', 'tokens' => []];
} else {
$extend['tokens'][] = $index;
$extend['content'] .= $tokens[$index]->getContent();
}

$index = $tokens->getNextMeaningfulToken($index);
}
}

/**
* @param array<string, string> $uses
*/
private function fixCatch(Tokens $tokens, int $index, array $uses, string $namespaceName): void
{
$index = $tokens->getNextMeaningfulToken($index); // '('
$index = $tokens->getNextMeaningfulToken($index); // first part of first exception class to be caught

$caughtExceptionClass = ['content' => '', 'tokens' => []];

while (true) {
if ($tokens[$index]->equalsAny([')', [T_VARIABLE], [CT::T_TYPE_ALTERNATION]])) {
if (0 === \count($caughtExceptionClass['tokens'])) {
break;
}

$this->shortenClassIfPossible($tokens, $caughtExceptionClass, $uses, $namespaceName);

if ($tokens[$index]->equals(')')) {
break;
}

$caughtExceptionClass = ['content' => '', 'tokens' => []];
} else {
$caughtExceptionClass['tokens'][] = $index;
$caughtExceptionClass['content'] .= $tokens[$index]->getContent();
}

$index = $tokens->getNextMeaningfulToken($index);
}
}

/**
* @param array<string, string> $uses
*/
private function fixClassStaticAccess(Tokens $tokens, int $index, array $uses, string $namespaceName): void
{
$classConstantRef = ['content' => '', 'tokens' => []];

while (true) {
$index = $tokens->getPrevMeaningfulToken($index);

if ($tokens[$index]->equalsAny([[T_STRING], [T_NS_SEPARATOR]])) {
$classConstantRef['tokens'][] = $index;
$classConstantRef['content'] = $tokens[$index]->getContent().$classConstantRef['content'];
} else {
$classConstantRef['tokens'] = array_reverse($classConstantRef['tokens']);
$this->shortenClassIfPossible($tokens, $classConstantRef, $uses, $namespaceName);

break;
}
}
}

/**
* @param array{content: string, tokens: array<int>} $class
* @param array<string, string> $uses
*/
private function shortenClassIfPossible(Tokens $tokens, array $class, array $uses, string $namespaceName): void
{
$longTypeContent = $class['content'];

if (str_starts_with($longTypeContent, '\\')) {
$typeName = substr($longTypeContent, 1);
$typeNameLower = strtolower($typeName);

if (isset($uses[$typeNameLower])) {
// if the type without leading "\" equals any of the full "uses" long names, it can be replaced with the short one
$this->replaceClassWithShort($tokens, $class, $uses[$typeNameLower]);
} elseif ('' === $namespaceName) {
if (true === $this->configuration['leading_backslash_in_global_namespace']) {
// if we are in the global namespace and the type is not imported the leading '\' can be removed
$inUses = false;

foreach ($uses as $useShortName) {
if (strtolower($useShortName) === $typeNameLower) {
$inUses = true;

break;
}
}

if (!$inUses) {
$this->replaceClassWithShort($tokens, $class, $typeName);
}
}
} elseif (
$typeNameLower !== $namespaceName
&& str_starts_with($typeNameLower, $namespaceName)
&& '\\' === $typeNameLower[\strlen($namespaceName)]
) {
// 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);
$this->replaceClassWithShort($tokens, $class, $typeNameShort);
}
} // else: no shorter type possible
}

/**
* @param array<string, string> $uses
*/
Expand Down Expand Up @@ -290,6 +454,24 @@ private function determineShortType(string $typeName, array $uses, string $names
return null;
}

/**
* @param array{content: string, tokens: array<int>} $class
*/
private function replaceClassWithShort(Tokens $tokens, array $class, string $short): void
{
$i = 0; // override the tokens

foreach ($this->namespacedStringToTokens($short) as $shortToken) {
$tokens[$class['tokens'][$i]] = $shortToken;
++$i;
}

// clear the leftovers
for ($j = \count($class['tokens']) - 1; $j >= $i; --$j) {
$tokens->clearTokenAndMergeSurroundingWhitespace($class['tokens'][$j]);
}
}

/**
* @return iterable<string, array{int, int}>
*/
Expand Down
4 changes: 2 additions & 2 deletions tests/Cache/NullCacheManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ final class NullCacheManagerTest extends TestCase
{
public function testIsFinal(): void
{
$reflection = new \ReflectionClass(\PhpCsFixer\Cache\NullCacheManager::class);
$reflection = new \ReflectionClass(NullCacheManager::class);

self::assertTrue($reflection->isFinal());
}

public function testImplementsCacheManagerInterface(): void
{
$reflection = new \ReflectionClass(\PhpCsFixer\Cache\NullCacheManager::class);
$reflection = new \ReflectionClass(NullCacheManager::class);

self::assertTrue($reflection->implementsInterface(\PhpCsFixer\Cache\CacheManagerInterface::class));
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Cache/SignatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ final class SignatureTest extends TestCase
{
public function testIsFinal(): void
{
$reflection = new \ReflectionClass(\PhpCsFixer\Cache\Signature::class);
$reflection = new \ReflectionClass(Signature::class);

self::assertTrue($reflection->isFinal());
}

public function testImplementsSignatureInterface(): void
{
$reflection = new \ReflectionClass(\PhpCsFixer\Cache\Signature::class);
$reflection = new \ReflectionClass(Signature::class);

self::assertTrue($reflection->implementsInterface(\PhpCsFixer\Cache\SignatureInterface::class));
}
Expand Down

0 comments on commit 1bc8e82

Please sign in to comment.