Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
peripherad
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
Fixer
/
Import
:
GroupImportFixer.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare(strict_types=1); /* * This file is part of PHP CS Fixer. * * (c) Fabien Potencier <fabien@symfony.com> * Dariusz RumiĆski <dariusz.ruminski@gmail.com> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Fixer\Import; use PhpCsFixer\AbstractFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\ConfigurableFixerTrait; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis; use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; use PhpCsFixer\Tokenizer\CT; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Utils; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; /** * @phpstan-type _AutogeneratedInputConfiguration array{ * group_types?: list<string>, * } * @phpstan-type _AutogeneratedComputedConfiguration array{ * group_types: list<string>, * } * * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> * * @author Volodymyr Kupriienko <vldmr.kuprienko@gmail.com> * @author Greg Korba <greg@codito.dev> * * @no-named-arguments Parameter names are not covered by the backward compatibility promise. */ final class GroupImportFixer extends AbstractFixer implements ConfigurableFixerInterface { /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */ use ConfigurableFixerTrait; /** @internal */ public const GROUP_CLASSY = 'classy'; /** @internal */ public const GROUP_CONSTANTS = 'constants'; /** @internal */ public const GROUP_FUNCTIONS = 'functions'; public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'There MUST be group use for the same namespaces.', [ new CodeSample( "<?php\nuse Foo\\Bar;\nuse Foo\\Baz;\n", ), new CodeSample( <<<'PHP' <?php use A\Foo; use function B\foo; use A\Bar; use function B\bar; PHP, ['group_types' => [self::GROUP_CLASSY]], ), ], ); } public function isCandidate(Tokens $tokens): bool { return $tokens->isTokenKindFound(\T_USE); } protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { $allowedTypes = [self::GROUP_CLASSY, self::GROUP_FUNCTIONS, self::GROUP_CONSTANTS]; return new FixerConfigurationResolver([ (new FixerOptionBuilder('group_types', 'Defines the order of import types.')) ->setAllowedTypes(['string[]']) ->setAllowedValues([static function (array $types) use ($allowedTypes): bool { foreach ($types as $type) { if (!\in_array($type, $allowedTypes, true)) { throw new InvalidOptionsException( \sprintf( 'Invalid group type: %s, allowed types: %s.', $type, Utils::naturalLanguageJoin($allowedTypes), ), ); } } return true; }]) ->setDefault($allowedTypes) ->getOption(), ]); } protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { $useWithSameNamespaces = $this->getSameNamespacesByType($tokens); if ([] === $useWithSameNamespaces) { return; } $typeMap = [ NamespaceUseAnalysis::TYPE_CLASS => self::GROUP_CLASSY, NamespaceUseAnalysis::TYPE_FUNCTION => self::GROUP_FUNCTIONS, NamespaceUseAnalysis::TYPE_CONSTANT => self::GROUP_CONSTANTS, ]; // As a first step we need to remove all the use statements for the enabled import types. // We can't add new group imports yet, because we need to operate on previously determined token indices for all types. foreach ($useWithSameNamespaces as $type => $uses) { if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) { continue; } $this->removeSingleUseStatements($uses, $tokens); } foreach ($useWithSameNamespaces as $type => $uses) { if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) { continue; } $this->addGroupUseStatements($uses, $tokens); } } /** * Gets namespace use analyzers with same namespaces. * * @return array<NamespaceUseAnalysis::TYPE_*, non-empty-list<NamespaceUseAnalysis>> */ private function getSameNamespacesByType(Tokens $tokens): array { $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens); if (0 === \count($useDeclarations)) { return []; } $allNamespaceAndType = array_map( fn (NamespaceUseAnalysis $useDeclaration): string => $this->getNamespaceNameWithSlash($useDeclaration).$useDeclaration->getType(), $useDeclarations, ); $sameNamespaces = array_filter(array_count_values($allNamespaceAndType), static fn (int $count): bool => $count > 1); $sameNamespaces = array_keys($sameNamespaces); $sameNamespaceAnalysis = array_filter($useDeclarations, function (NamespaceUseAnalysis $useDeclaration) use ($sameNamespaces): bool { $namespaceNameAndType = $this->getNamespaceNameWithSlash($useDeclaration).$useDeclaration->getType(); return \in_array($namespaceNameAndType, $sameNamespaces, true); }); usort($sameNamespaceAnalysis, function (NamespaceUseAnalysis $a, NamespaceUseAnalysis $b): int { $namespaceA = $this->getNamespaceNameWithSlash($a); $namespaceB = $this->getNamespaceNameWithSlash($b); $namespaceDifference = \strlen($namespaceA) <=> \strlen($namespaceB); return 0 !== $namespaceDifference ? $namespaceDifference : $a->getFullName() <=> $b->getFullName(); }); $sameNamespaceAnalysisByType = []; foreach ($sameNamespaceAnalysis as $analysis) { $sameNamespaceAnalysisByType[$analysis->getType()][] = $analysis; } ksort($sameNamespaceAnalysisByType); return $sameNamespaceAnalysisByType; } /** * @param list<NamespaceUseAnalysis> $statements */ private function removeSingleUseStatements(array $statements, Tokens $tokens): void { foreach ($statements as $useDeclaration) { $index = $useDeclaration->getStartIndex(); $endIndex = $useDeclaration->getEndIndex(); $useStatementTokens = [\T_USE, \T_WHITESPACE, \T_STRING, \T_NS_SEPARATOR, \T_AS, CT::T_CONST_IMPORT, CT::T_FUNCTION_IMPORT]; while ($index !== $endIndex) { if ($tokens[$index]->isGivenKind($useStatementTokens)) { $tokens->clearAt($index); } ++$index; } if (isset($tokens[$index]) && $tokens[$index]->equals(';')) { $tokens->clearAt($index); } ++$index; if (isset($tokens[$index]) && $tokens[$index]->isGivenKind(\T_WHITESPACE)) { $tokens->clearAt($index); } } } /** * @param list<NamespaceUseAnalysis> $statements */ private function addGroupUseStatements(array $statements, Tokens $tokens): void { $currentUseDeclaration = null; $insertIndex = $statements[0]->getStartIndex(); // If group import was inserted in place of removed imports, it may have more tokens than before, // and indices stored in imports of another type can be out-of-sync, and can point in the middle of group import. // Let's move the pointer to the closest empty token (erased single import). if (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent()) { do { ++$insertIndex; } while (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent()); } foreach ($statements as $index => $useDeclaration) { if ($this->areDeclarationsDifferent($currentUseDeclaration, $useDeclaration)) { $currentUseDeclaration = $useDeclaration; $insertIndex += $this->createNewGroup( $tokens, $insertIndex, $useDeclaration, rtrim($this->getNamespaceNameWithSlash($currentUseDeclaration), '\\'), ); } else { $newTokens = [ new Token(','), new Token([\T_WHITESPACE, ' ']), ]; if ($useDeclaration->isAliased()) { $tokens->insertAt($insertIndex, $newTokens); $insertIndex += \count($newTokens); $newTokens = []; $insertIndex += $this->insertToGroupUseWithAlias($tokens, $insertIndex, $useDeclaration); } $newTokens[] = new Token([\T_STRING, $useDeclaration->getShortName()]); if (!isset($statements[$index + 1]) || $this->areDeclarationsDifferent($currentUseDeclaration, $statements[$index + 1])) { $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']); $newTokens[] = new Token(';'); $newTokens[] = new Token([\T_WHITESPACE, "\n"]); } $tokens->insertAt($insertIndex, $newTokens); $insertIndex += \count($newTokens); } } } private function getNamespaceNameWithSlash(NamespaceUseAnalysis $useDeclaration): string { $position = strrpos($useDeclaration->getFullName(), '\\'); if (false === $position || 0 === $position) { return $useDeclaration->getFullName(); } return substr($useDeclaration->getFullName(), 0, $position + 1); } /** * Insert use with alias to the group. */ private function insertToGroupUseWithAlias(Tokens $tokens, int $insertIndex, NamespaceUseAnalysis $useDeclaration): int { $newTokens = [ new Token([\T_STRING, substr($useDeclaration->getFullName(), (int) strripos($useDeclaration->getFullName(), '\\') + 1)]), new Token([\T_WHITESPACE, ' ']), new Token([\T_AS, 'as']), new Token([\T_WHITESPACE, ' ']), ]; $tokens->insertAt($insertIndex, $newTokens); return \count($newTokens); } /** * Creates new use statement group. */ private function createNewGroup(Tokens $tokens, int $insertIndex, NamespaceUseAnalysis $useDeclaration, string $currentNamespace): int { $insertedTokens = 0; $newTokens = [ new Token([\T_USE, 'use']), new Token([\T_WHITESPACE, ' ']), ]; if ($useDeclaration->isFunction() || $useDeclaration->isConstant()) { $importStatementParams = $useDeclaration->isFunction() ? [CT::T_FUNCTION_IMPORT, 'function'] : [CT::T_CONST_IMPORT, 'const']; $newTokens[] = new Token($importStatementParams); $newTokens[] = new Token([\T_WHITESPACE, ' ']); } $namespaceParts = explode('\\', $currentNamespace); foreach ($namespaceParts as $part) { $newTokens[] = new Token([\T_STRING, $part]); $newTokens[] = new Token([\T_NS_SEPARATOR, '\\']); } $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_OPEN, '{']); $newTokensCount = \count($newTokens); $tokens->insertAt($insertIndex, $newTokens); $insertedTokens += $newTokensCount; $insertIndex += $newTokensCount; if ($useDeclaration->isAliased()) { $inserted = $this->insertToGroupUseWithAlias($tokens, $insertIndex + 1, $useDeclaration) + 1; $insertedTokens += $inserted; $insertIndex += $inserted; } $tokens->insertAt($insertIndex, new Token([\T_STRING, $useDeclaration->getShortName()])); return ++$insertedTokens; } /** * Check if namespace use analyses are different. */ private function areDeclarationsDifferent(?NamespaceUseAnalysis $analysis1, ?NamespaceUseAnalysis $analysis2): bool { if (null === $analysis1 || null === $analysis2) { return true; } $namespaceName1 = $this->getNamespaceNameWithSlash($analysis1); $namespaceName2 = $this->getNamespaceNameWithSlash($analysis2); return $namespaceName1 !== $namespaceName2 || $analysis1->getType() !== $analysis2->getType(); } }