Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FullyQualifiedStrictTypesFixer - fix same classname is imported from … #6197

Merged
merged 1 commit into from Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.