-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(GroupImportFixer): Ability to configure which type of imports sh…
…ould be grouped (#8046)
- Loading branch information
Showing
5 changed files
with
312 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,10 @@ | |
namespace PhpCsFixer\Fixer\Import; | ||
|
||
use PhpCsFixer\AbstractFixer; | ||
use PhpCsFixer\Fixer\ConfigurableFixerInterface; | ||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; | ||
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; | ||
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; | ||
use PhpCsFixer\FixerDefinition\CodeSample; | ||
use PhpCsFixer\FixerDefinition\FixerDefinition; | ||
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; | ||
|
@@ -23,12 +27,24 @@ | |
use PhpCsFixer\Tokenizer\CT; | ||
use PhpCsFixer\Tokenizer\Token; | ||
use PhpCsFixer\Tokenizer\Tokens; | ||
use PhpCsFixer\Utils; | ||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; | ||
|
||
/** | ||
* @author Volodymyr Kupriienko <[email protected]> | ||
* @author Greg Korba <[email protected]> | ||
*/ | ||
final class GroupImportFixer extends AbstractFixer | ||
final class GroupImportFixer extends AbstractFixer implements ConfigurableFixerInterface | ||
{ | ||
/** @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( | ||
|
@@ -37,6 +53,18 @@ public function getDefinition(): FixerDefinitionInterface | |
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]] | ||
), | ||
] | ||
); | ||
} | ||
|
@@ -46,24 +74,72 @@ 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->getSameNamespaces($tokens); | ||
$useWithSameNamespaces = $this->getSameNamespacesByType($tokens); | ||
|
||
if ([] === $useWithSameNamespaces) { | ||
return; | ||
} | ||
|
||
$this->removeSingleUseStatements($useWithSameNamespaces, $tokens); | ||
$this->addGroupUseStatements($useWithSameNamespaces, $tokens); | ||
$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 list<NamespaceUseAnalysis> | ||
* @return array<NamespaceUseAnalysis::TYPE_*, list<NamespaceUseAnalysis>> | ||
*/ | ||
private function getSameNamespaces(Tokens $tokens): array | ||
private function getSameNamespacesByType(Tokens $tokens): array | ||
{ | ||
$useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens); | ||
|
||
|
@@ -94,7 +170,14 @@ private function getSameNamespaces(Tokens $tokens): array | |
return 0 !== $namespaceDifference ? $namespaceDifference : $a->getFullName() <=> $b->getFullName(); | ||
}); | ||
|
||
return $sameNamespaceAnalysis; | ||
$sameNamespaceAnalysisByType = []; | ||
foreach ($sameNamespaceAnalysis as $analysis) { | ||
$sameNamespaceAnalysisByType[$analysis->getType()][] = $analysis; | ||
} | ||
|
||
ksort($sameNamespaceAnalysisByType); | ||
|
||
return $sameNamespaceAnalysisByType; | ||
} | ||
|
||
/** | ||
|
@@ -134,7 +217,16 @@ private function removeSingleUseStatements(array $statements, Tokens $tokens): v | |
private function addGroupUseStatements(array $statements, Tokens $tokens): void | ||
{ | ||
$currentUseDeclaration = null; | ||
$insertIndex = \array_slice($statements, -1)[0]->getEndIndex() + 1; | ||
$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)) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.