Skip to content

Commit

Permalink
bug #6197 FullyQualifiedStrictTypesFixer - fix same classname is impo…
Browse files Browse the repository at this point in the history
…rted from … (SpacePossum)

This PR was squashed before being merged into the master branch (closes #6197).

Discussion
----------

FullyQualifiedStrictTypesFixer - fix same classname is imported from …

…global and non-global namespace

closes #4726

there are multiple issues with this fixer, please have a look at the new logic and tests

Commits
-------

2e57360 FullyQualifiedStrictTypesFixer - fix same classname is imported from …
  • Loading branch information
SpacePossum committed Jan 22, 2022
2 parents 5a463a5 + 2e57360 commit d0d54d7
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 428 deletions.
9 changes: 9 additions & 0 deletions src/AbstractLinesBeforeNamespaceFixer.php
Expand Up @@ -42,22 +42,27 @@ protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expe
$precedingNewlines = 0;
$newlineInOpening = false;
$openingToken = null;

for ($i = 1; $i <= 2; ++$i) {
if (isset($tokens[$index - $i])) {
$token = $tokens[$index - $i];

if ($token->isGivenKind(T_OPEN_TAG)) {
$openingToken = $token;
$openingTokenIndex = $index - $i;
$newlineInOpening = str_contains($token->getContent(), "\n");

if ($newlineInOpening) {
++$precedingNewlines;
}

break;
}

if (false === $token->isGivenKind(T_WHITESPACE)) {
break;
}

$precedingNewlines += substr_count($token->getContent(), "\n");
}
}
Expand All @@ -74,6 +79,7 @@ protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expe
if ($previous->isWhitespace()) {
$tokens->clearAt($previousIndex);
}

// Remove new lines in opening token
if ($newlineInOpening) {
$tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, rtrim($openingToken->getContent()).' ']);
Expand All @@ -84,13 +90,15 @@ protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expe

$lineEnding = $this->whitespacesConfig->getLineEnding();
$newlinesForWhitespaceToken = $expectedMax;

if (null !== $openingToken) {
// Use the configured line ending for the PHP opening tag
$content = rtrim($openingToken->getContent());
$newContent = $content.$lineEnding;
$tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, $newContent]);
--$newlinesForWhitespaceToken;
}

if (0 === $newlinesForWhitespaceToken) {
// We have all the needed new lines in the opening tag
if ($previous->isWhitespace()) {
Expand All @@ -100,6 +108,7 @@ protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expe

return;
}

if ($previous->isWhitespace()) {
// Fix the previous whitespace token
$tokens[$previousIndex] = new Token([T_WHITESPACE, str_repeat($lineEnding, $newlinesForWhitespaceToken).substr($previous->getContent(), strrpos($previous->getContent(), "\n") + 1)]);
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/ClassNotation/OrderedInterfacesFixer.php
Expand Up @@ -154,7 +154,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
while ($interfaceTokens->offsetExists($actualInterfaceIndex)) {
$token = $interfaceTokens[$actualInterfaceIndex];

if (null === $token || $token->isComment() || $token->isWhitespace()) {
if ($token->isComment() || $token->isWhitespace()) {
break;
}

Expand Down
145 changes: 87 additions & 58 deletions src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
Expand Up @@ -23,8 +23,7 @@
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Generator\NamespacedStringTokenGenerator;
use PhpCsFixer\Tokenizer\Resolver\TypeShortNameResolver;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
Expand Down Expand Up @@ -87,114 +86,144 @@ public function getPriority(): int
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_FUNCTION) && (
\count((new NamespacesAnalyzer())->getDeclarations($tokens)) > 0
|| \count((new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens)) > 0
);
return $tokens->isTokenKindFound(T_FUNCTION);
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$lastIndex = $tokens->count() - 1;
$namespacesAnalyzer = new NamespacesAnalyzer();
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
$functionsAnalyzer = new FunctionsAnalyzer();

for ($index = $lastIndex; $index >= 0; --$index) {
if (!$tokens[$index]->isGivenKind(T_FUNCTION)) {
continue;
foreach ($namespacesAnalyzer->getDeclarations($tokens) as $namespace) {
$namespaceName = strtolower($namespace->getFullName());
$uses = [];

foreach ($namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace) as $use) {
$uses[strtolower(ltrim($use->getFullName(), '\\'))] = $use->getShortName();
}

// Return types are only available since PHP 7.0
$this->fixFunctionReturnType($tokens, $index);
$this->fixFunctionArguments($tokens, $index);
for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); ++$index) {
if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
$this->fixFunction($functionsAnalyzer, $tokens, $index, $uses, $namespaceName);
}
}
}
}

private function fixFunctionArguments(Tokens $tokens, int $index): void
/**
* @param array<string, string> $uses
*/
private function fixFunction(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index, array $uses, string $namespaceName): void
{
$arguments = (new FunctionsAnalyzer())->getFunctionArguments($tokens, $index);
$arguments = $functionsAnalyzer->getFunctionArguments($tokens, $index);

foreach ($arguments as $argument) {
if (!$argument->hasTypeAnalysis()) {
continue;
if ($argument->hasTypeAnalysis()) {
$this->replaceByShortType($tokens, $argument->getTypeAnalysis(), $uses, $namespaceName);
}

$this->detectAndReplaceTypeWithShortType($tokens, $argument->getTypeAnalysis());
}
}

private function fixFunctionReturnType(Tokens $tokens, int $index): void
{
$returnType = (new FunctionsAnalyzer())->getFunctionReturnType($tokens, $index);
$returnTypeAnalysis = $functionsAnalyzer->getFunctionReturnType($tokens, $index);

if (null === $returnType) {
return;
if (null !== $returnTypeAnalysis) {
$this->replaceByShortType($tokens, $returnTypeAnalysis, $uses, $namespaceName);
}

$this->detectAndReplaceTypeWithShortType($tokens, $returnType);
}

private function detectAndReplaceTypeWithShortType(
Tokens $tokens,
TypeAnalysis $type
): void {
/**
* @param array<string, string> $uses
*/
private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $uses, string $namespaceName): void
{
if ($type->isReservedType()) {
return;
}

$typeStartIndex = $type->getStartIndex();

if ($tokens[$typeStartIndex]->isGivenKind(CT::T_NULLABLE_TYPE)) {
$typeStartIndex = $tokens->getNextMeaningfulToken($typeStartIndex);
}

foreach ($this->getSimpleTypes($tokens, $typeStartIndex, $type->getEndIndex()) as $simpleType) {
$typeName = $tokens->generatePartialCode($simpleType['start'], $simpleType['end']);
$namespaceNameLength = \strlen($namespaceName);
$types = $this->getTypes($tokens, $typeStartIndex, $type->getEndIndex());

foreach ($types as $typeName => [$startIndex, $endIndex]) {
if (!str_starts_with($typeName, '\\')) {
continue;
continue; // no shorter type possible
}

$shortType = (new TypeShortNameResolver())->resolve($tokens, $typeName);
if ($shortType === $typeName) {
continue;
$typeName = substr($typeName, 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
$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($uses[$typeNameLower]));
} elseif ('' === $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) {
continue 2;
}
}

$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeName));
} 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);
$tokens->overrideRange($startIndex, $endIndex, $this->namespacedStringToTokens($typeNameShort));
}

$shortType = (new NamespacedStringTokenGenerator())->generate($shortType);

$tokens->overrideRange(
$simpleType['start'],
$simpleType['end'],
$shortType
);
}
}

/**
* @return \Generator<array<int>>
*/
private function getSimpleTypes(Tokens $tokens, int $startIndex, int $endIndex): iterable
private function getTypes(Tokens $tokens, int $index, int $endIndex): iterable
{
$index = $startIndex;
$index = $typeStartIndex = $typeEndIndex = $tokens->getNextMeaningfulToken($index - 1);
$type = $tokens[$index]->getContent();

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

if (null === $startIndex) {
$startIndex = $index;
if ($tokens[$index]->isGivenKind([CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION])) {
yield $type => [$typeStartIndex, $typeEndIndex];

$index = $typeStartIndex = $typeEndIndex = $tokens->getNextMeaningfulToken($index);
$type = $tokens[$index]->getContent();

continue;
}

if ($index >= $endIndex) {
yield ['start' => $startIndex, 'end' => $index];
if ($index > $endIndex || !$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
yield $type => [$typeStartIndex, $typeEndIndex];

break;
}

if ($tokens[$index]->isGivenKind([CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION])) {
yield ['start' => $startIndex, 'end' => $prevIndex];
$startIndex = null;
$typeEndIndex = $index;
$type .= $tokens[$index]->getContent();
}
}

/**
* @return Token[]
*/
private function namespacedStringToTokens(string $input): array
{
$tokens = [];
$parts = explode('\\', $input);

foreach ($parts as $index => $part) {
$tokens[] = new Token([T_STRING, $part]);

if ($index !== \count($parts) - 1) {
$tokens[] = new Token([T_NS_SEPARATOR, '\\']);
}
}

return $tokens;
}
}
43 changes: 0 additions & 43 deletions src/Tokenizer/Generator/NamespacedStringTokenGenerator.php

This file was deleted.

0 comments on commit d0d54d7

Please sign in to comment.