|
@@ -34,24 +34,28 @@ use PhpCsFixer\Tokenizer\Tokens;
|
|
|
* @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
|
|
@@ -71,6 +75,12 @@ class SomeClass
|
|
|
public function getBaz() {
|
|
|
return $this->baz;
|
|
|
}
|
|
|
+
|
|
|
+ public function doX(\Foo\Bar $foo, \Exception $e): \Foo\Bar\Baz
|
|
|
+ {
|
|
|
+ try {}
|
|
|
+ catch (\Foo\SomeException $e) {}
|
|
|
+ }
|
|
|
}
|
|
|
'
|
|
|
),
|
|
@@ -83,6 +93,23 @@ class SomeClass
|
|
|
{
|
|
|
}
|
|
|
}
|
|
|
+',
|
|
|
+ ['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]
|
|
|
),
|
|
@@ -103,7 +130,14 @@ class SomeClass
|
|
|
|
|
|
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
|
|
@@ -135,6 +169,12 @@ class SomeClass
|
|
|
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)) {
|
|
@@ -200,6 +240,130 @@ class SomeClass
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @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
|
|
|
*/
|
|
@@ -290,6 +454,24 @@ class SomeClass
|
|
|
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}>
|
|
|
*/
|