Skip to content

Commit

Permalink
feat(GroupImportFixer): Ability to configure which type of imports sh…
Browse files Browse the repository at this point in the history
…ould be grouped (#8046)
  • Loading branch information
Wirone authored May 26, 2024
1 parent 6cad43d commit 17050b2
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 18 deletions.
31 changes: 31 additions & 0 deletions doc/rules/import/group_import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ Rule ``group_import``

There MUST be group use for the same namespaces.

Configuration
-------------

``group_types``
~~~~~~~~~~~~~~~

Defines the order of import types.

Allowed types: ``list<string>``

Default value: ``['classy', 'functions', 'constants']``

Examples
--------

Example #1
~~~~~~~~~~

*Default* configuration.

.. code-block:: diff
--- Original
Expand All @@ -18,6 +32,23 @@ Example #1
-use Foo\Bar;
-use Foo\Baz;
+use Foo\{Bar, Baz};
Example #2
~~~~~~~~~~

With configuration: ``['group_types' => ['classy']]``.

.. code-block:: diff
--- Original
+++ New
<?php
-use A\Foo;
use function B\foo;
-use A\Bar;
+use A\{Bar, Foo};
use function B\bar;
References
----------

Expand Down
2 changes: 1 addition & 1 deletion phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ parameters:
-
message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
path: tests
count: 1014
count: 1013
tipsOfTheDay: false
tmpDir: dev-tools/phpstan/cache
108 changes: 100 additions & 8 deletions src/Fixer/Import/GroupImportFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <vldmr.kuprienko@gmail.com>
* @author Greg Korba <greg@codito.dev>
*/
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(
Expand All @@ -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]]
),
]
);
}
Expand All @@ -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);

Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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)) {
Expand Down
5 changes: 5 additions & 0 deletions src/Tokenizer/Analyzer/Analysis/NamespaceUseAnalysis.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ final class NamespaceUseAnalysis implements StartEndTokenAwareAnalysis

/**
* The type of import: class, function or constant.
*
* @var self::TYPE_*
*/
private int $type;

Expand Down Expand Up @@ -140,6 +142,9 @@ public function getChunkEndIndex(): ?int
return $this->chunkEndIndex;
}

/**
* @return self::TYPE_*
*/
public function getType(): int
{
return $this->type;
Expand Down
Loading

0 comments on commit 17050b2

Please sign in to comment.