diff --git a/src/AbstractLinesBeforeNamespaceFixer.php b/src/AbstractLinesBeforeNamespaceFixer.php index 3fdd94f7f92..f574b97040c 100644 --- a/src/AbstractLinesBeforeNamespaceFixer.php +++ b/src/AbstractLinesBeforeNamespaceFixer.php @@ -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"); } } @@ -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()).' ']); @@ -84,6 +90,7 @@ 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()); @@ -91,6 +98,7 @@ protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expe $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()) { @@ -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)]); diff --git a/src/Fixer/ClassNotation/OrderedInterfacesFixer.php b/src/Fixer/ClassNotation/OrderedInterfacesFixer.php index 2fc1e366928..a27bb38ff07 100644 --- a/src/Fixer/ClassNotation/OrderedInterfacesFixer.php +++ b/src/Fixer/ClassNotation/OrderedInterfacesFixer.php @@ -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; } diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php index 436951ec732..55dc6471e2e 100644 --- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php +++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php @@ -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; /** @@ -87,10 +86,7 @@ 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); } /** @@ -98,103 +94,136 @@ public function isCandidate(Tokens $tokens): bool */ 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 $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 $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> - */ - 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; } } diff --git a/src/Tokenizer/Generator/NamespacedStringTokenGenerator.php b/src/Tokenizer/Generator/NamespacedStringTokenGenerator.php deleted file mode 100644 index d78c5106bc7..00000000000 --- a/src/Tokenizer/Generator/NamespacedStringTokenGenerator.php +++ /dev/null @@ -1,43 +0,0 @@ - - * Dariusz Rumiński - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace PhpCsFixer\Tokenizer\Generator; - -use PhpCsFixer\Tokenizer\Token; - -/** - * @internal - */ -final class NamespacedStringTokenGenerator -{ - /** - * Parse a string that contains a namespace into tokens. - * - * @return Token[] - */ - public function generate(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; - } -} diff --git a/src/Tokenizer/Resolver/TypeShortNameResolver.php b/src/Tokenizer/Resolver/TypeShortNameResolver.php deleted file mode 100644 index d07a0d5503c..00000000000 --- a/src/Tokenizer/Resolver/TypeShortNameResolver.php +++ /dev/null @@ -1,92 +0,0 @@ - - * Dariusz Rumiński - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace PhpCsFixer\Tokenizer\Resolver; - -use PhpCsFixer\Preg; -use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; -use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer; -use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; -use PhpCsFixer\Tokenizer\Tokens; - -/** - * @internal - */ -final class TypeShortNameResolver -{ - /** - * This method will resolve the shortName of a FQCN if possible or otherwise return the inserted type name. - * E.g.: use Foo\Bar => "Bar". - */ - public function resolve(Tokens $tokens, string $typeName): string - { - // First match explicit imports: - $useMap = $this->getUseMapFromTokens($tokens); - foreach ($useMap as $shortName => $fullName) { - $regex = '/^\\\\?'.preg_quote($fullName, '/').'$/'; - if (Preg::match($regex, $typeName)) { - return $shortName; - } - } - - // Next try to match (partial) classes inside the same namespace - // For now only support one namespace per file: - $namespaces = $this->getNamespacesFromTokens($tokens); - if (1 === \count($namespaces)) { - foreach ($namespaces as $fullName) { - $matches = []; - $regex = '/^\\\\?'.preg_quote($fullName, '/').'\\\\(?P.+)$/'; - if (Preg::match($regex, $typeName, $matches)) { - return $matches['className']; - } - } - } - - // Next: Try to match partial use statements: - - foreach ($useMap as $shortName => $fullName) { - $matches = []; - $regex = '/^\\\\?'.preg_quote($fullName, '/').'\\\\(?P.+)$/'; - if (Preg::match($regex, $typeName, $matches)) { - return $shortName.'\\'.$matches['className']; - } - } - - return $typeName; - } - - /** - * @return array A list of all FQN namespaces in the file with the short name as key - */ - private function getNamespacesFromTokens(Tokens $tokens): array - { - return array_map(static function (NamespaceAnalysis $info): string { - return $info->getFullName(); - }, (new NamespacesAnalyzer())->getDeclarations($tokens)); - } - - /** - * @return array A list of all FQN use statements in the file with the short name as key - */ - private function getUseMapFromTokens(Tokens $tokens): array - { - $map = []; - - foreach ((new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens) as $useDeclaration) { - $map[$useDeclaration->getShortName()] = $useDeclaration->getFullName(); - } - - return $map; - } -} diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php index 65dbd95a4f9..81773433686 100644 --- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php +++ b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php @@ -17,7 +17,7 @@ use PhpCsFixer\Tests\Test\AbstractFixerTestCase; /** - * @author Dariusz Rumiński + * @author VeeWee * * @internal * @@ -26,27 +26,98 @@ final class FullyQualifiedStrictTypesFixerTest extends AbstractFixerTestCase { /** - * @dataProvider provideCodeWithReturnTypesCases - * @dataProvider provideCodeWithReturnTypesCasesWithNullableCases + * @dataProvider provideNewLogicCases */ - public function testCodeWithReturnTypes(string $expected, ?string $input = null): void + public function testNewLogic(string $expected, ?string $input): void { $this->doTest($expected, $input); } + public function provideNewLogicCases(): iterable + { + yield 'namespace === type name' => [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + 'doTest($expected, $input); } - public function provideCodeWithReturnTypesCases(): array + public function provideCodeWithReturnTypesCases(): iterable { - return [ - 'Import common strict types' => [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - 'doTest($expected, $input); + } + + public function provideCodeWithoutReturnTypesCases(): iterable + { + yield 'import from namespace and global' => [ + ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ - ' [ + ' [ + ' [ + ' - * Dariusz Rumiński - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace PhpCsFixer\Tests\Tokenizer\Generator; - -use PhpCsFixer\Tests\TestCase; -use PhpCsFixer\Tokenizer\Generator\NamespacedStringTokenGenerator; -use PhpCsFixer\Tokenizer\Token; - -/** - * @internal - * - * @covers \PhpCsFixer\Tokenizer\Generator\NamespacedStringTokenGenerator - */ -final class NamespacedStringTokenGeneratorTest extends TestCase -{ - /** - * @dataProvider provideGeneratorCases - */ - public function testGenerator(array $expected, string $input): void - { - $generator = new NamespacedStringTokenGenerator(); - - static::assertSame( - $expected, - array_map( - static function (Token $token): string { - return $token->getContent(); - }, - $generator->generate($input) - ) - ); - } - - public function provideGeneratorCases(): \Generator - { - yield [['test'], 'test']; - - yield [['Some', '\\', 'Namespace'], 'Some\\Namespace']; - - yield [['Some', '\\', 'Bigger', '\\', 'Namespace'], 'Some\\Bigger\\Namespace']; - } -} diff --git a/tests/Tokenizer/Resolver/TypeShortNameResolverTest.php b/tests/Tokenizer/Resolver/TypeShortNameResolverTest.php deleted file mode 100644 index f98e1a79bd7..00000000000 --- a/tests/Tokenizer/Resolver/TypeShortNameResolverTest.php +++ /dev/null @@ -1,84 +0,0 @@ - - * Dariusz Rumiński - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace PhpCsFixer\Tests\Tokenizer\Resolver; - -use PhpCsFixer\Tests\TestCase; -use PhpCsFixer\Tokenizer\Resolver\TypeShortNameResolver; -use PhpCsFixer\Tokenizer\Tokens; - -/** - * @internal - * - * @covers \PhpCsFixer\Tokenizer\Resolver\TypeShortNameResolver - */ -final class TypeShortNameResolverTest extends TestCase -{ - /** - * @dataProvider provideResolverCases - */ - public function testResolver(string $code, string $type, string $expected): void - { - $resolver = new TypeShortNameResolver(); - $tokens = Tokens::fromCode($code); - - static::assertSame($expected, $resolver->resolve($tokens, $type)); - } - - public function provideResolverCases(): array - { - return [ - [ - '