Skip to content

Commit 17050b2

Browse files
authored
feat(GroupImportFixer): Ability to configure which type of imports should be grouped (#8046)
1 parent 6cad43d commit 17050b2

File tree

5 files changed

+312
-18
lines changed

5 files changed

+312
-18
lines changed

doc/rules/import/group_import.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@ Rule ``group_import``
44

55
There MUST be group use for the same namespaces.
66

7+
Configuration
8+
-------------
9+
10+
``group_types``
11+
~~~~~~~~~~~~~~~
12+
13+
Defines the order of import types.
14+
15+
Allowed types: ``list<string>``
16+
17+
Default value: ``['classy', 'functions', 'constants']``
18+
719
Examples
820
--------
921

1022
Example #1
1123
~~~~~~~~~~
1224

25+
*Default* configuration.
26+
1327
.. code-block:: diff
1428
1529
--- Original
@@ -18,6 +32,23 @@ Example #1
1832
-use Foo\Bar;
1933
-use Foo\Baz;
2034
+use Foo\{Bar, Baz};
35+
36+
Example #2
37+
~~~~~~~~~~
38+
39+
With configuration: ``['group_types' => ['classy']]``.
40+
41+
.. code-block:: diff
42+
43+
--- Original
44+
+++ New
45+
<?php
46+
47+
-use A\Foo;
48+
use function B\foo;
49+
-use A\Bar;
50+
+use A\{Bar, Foo};
51+
use function B\bar;
2152
References
2253
----------
2354

phpstan.dist.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ parameters:
5555
-
5656
message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
5757
path: tests
58-
count: 1014
58+
count: 1013
5959
tipsOfTheDay: false
6060
tmpDir: dev-tools/phpstan/cache

src/Fixer/Import/GroupImportFixer.php

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
namespace PhpCsFixer\Fixer\Import;
1616

1717
use PhpCsFixer\AbstractFixer;
18+
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
20+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
21+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
1822
use PhpCsFixer\FixerDefinition\CodeSample;
1923
use PhpCsFixer\FixerDefinition\FixerDefinition;
2024
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
@@ -23,12 +27,24 @@
2327
use PhpCsFixer\Tokenizer\CT;
2428
use PhpCsFixer\Tokenizer\Token;
2529
use PhpCsFixer\Tokenizer\Tokens;
30+
use PhpCsFixer\Utils;
31+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
2632

2733
/**
2834
* @author Volodymyr Kupriienko <[email protected]>
35+
* @author Greg Korba <[email protected]>
2936
*/
30-
final class GroupImportFixer extends AbstractFixer
37+
final class GroupImportFixer extends AbstractFixer implements ConfigurableFixerInterface
3138
{
39+
/** @internal */
40+
public const GROUP_CLASSY = 'classy';
41+
42+
/** @internal */
43+
public const GROUP_CONSTANTS = 'constants';
44+
45+
/** @internal */
46+
public const GROUP_FUNCTIONS = 'functions';
47+
3248
public function getDefinition(): FixerDefinitionInterface
3349
{
3450
return new FixerDefinition(
@@ -37,6 +53,18 @@ public function getDefinition(): FixerDefinitionInterface
3753
new CodeSample(
3854
"<?php\nuse Foo\\Bar;\nuse Foo\\Baz;\n"
3955
),
56+
new CodeSample(
57+
<<<'PHP'
58+
<?php
59+
60+
use A\Foo;
61+
use function B\foo;
62+
use A\Bar;
63+
use function B\bar;
64+
65+
PHP,
66+
['group_types' => [self::GROUP_CLASSY]]
67+
),
4068
]
4169
);
4270
}
@@ -46,24 +74,72 @@ public function isCandidate(Tokens $tokens): bool
4674
return $tokens->isTokenKindFound(T_USE);
4775
}
4876

77+
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
78+
{
79+
$allowedTypes = [self::GROUP_CLASSY, self::GROUP_FUNCTIONS, self::GROUP_CONSTANTS];
80+
81+
return new FixerConfigurationResolver([
82+
(new FixerOptionBuilder('group_types', 'Defines the order of import types.'))
83+
->setAllowedTypes(['string[]'])
84+
->setAllowedValues([static function (array $types) use ($allowedTypes): bool {
85+
foreach ($types as $type) {
86+
if (!\in_array($type, $allowedTypes, true)) {
87+
throw new InvalidOptionsException(
88+
sprintf(
89+
'Invalid group type: %s, allowed types: %s.',
90+
$type,
91+
Utils::naturalLanguageJoin($allowedTypes)
92+
)
93+
);
94+
}
95+
}
96+
97+
return true;
98+
}])
99+
->setDefault($allowedTypes)
100+
->getOption(),
101+
]);
102+
}
103+
49104
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
50105
{
51-
$useWithSameNamespaces = $this->getSameNamespaces($tokens);
106+
$useWithSameNamespaces = $this->getSameNamespacesByType($tokens);
52107

53108
if ([] === $useWithSameNamespaces) {
54109
return;
55110
}
56111

57-
$this->removeSingleUseStatements($useWithSameNamespaces, $tokens);
58-
$this->addGroupUseStatements($useWithSameNamespaces, $tokens);
112+
$typeMap = [
113+
NamespaceUseAnalysis::TYPE_CLASS => self::GROUP_CLASSY,
114+
NamespaceUseAnalysis::TYPE_FUNCTION => self::GROUP_FUNCTIONS,
115+
NamespaceUseAnalysis::TYPE_CONSTANT => self::GROUP_CONSTANTS,
116+
];
117+
118+
// As a first step we need to remove all the use statements for the enabled import types.
119+
// We can't add new group imports yet, because we need to operate on previously determined token indices for all types.
120+
foreach ($useWithSameNamespaces as $type => $uses) {
121+
if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) {
122+
continue;
123+
}
124+
125+
$this->removeSingleUseStatements($uses, $tokens);
126+
}
127+
128+
foreach ($useWithSameNamespaces as $type => $uses) {
129+
if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) {
130+
continue;
131+
}
132+
133+
$this->addGroupUseStatements($uses, $tokens);
134+
}
59135
}
60136

61137
/**
62138
* Gets namespace use analyzers with same namespaces.
63139
*
64-
* @return list<NamespaceUseAnalysis>
140+
* @return array<NamespaceUseAnalysis::TYPE_*, list<NamespaceUseAnalysis>>
65141
*/
66-
private function getSameNamespaces(Tokens $tokens): array
142+
private function getSameNamespacesByType(Tokens $tokens): array
67143
{
68144
$useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
69145

@@ -94,7 +170,14 @@ private function getSameNamespaces(Tokens $tokens): array
94170
return 0 !== $namespaceDifference ? $namespaceDifference : $a->getFullName() <=> $b->getFullName();
95171
});
96172

97-
return $sameNamespaceAnalysis;
173+
$sameNamespaceAnalysisByType = [];
174+
foreach ($sameNamespaceAnalysis as $analysis) {
175+
$sameNamespaceAnalysisByType[$analysis->getType()][] = $analysis;
176+
}
177+
178+
ksort($sameNamespaceAnalysisByType);
179+
180+
return $sameNamespaceAnalysisByType;
98181
}
99182

100183
/**
@@ -134,7 +217,16 @@ private function removeSingleUseStatements(array $statements, Tokens $tokens): v
134217
private function addGroupUseStatements(array $statements, Tokens $tokens): void
135218
{
136219
$currentUseDeclaration = null;
137-
$insertIndex = \array_slice($statements, -1)[0]->getEndIndex() + 1;
220+
$insertIndex = $statements[0]->getStartIndex();
221+
222+
// If group import was inserted in place of removed imports, it may have more tokens than before,
223+
// and indices stored in imports of another type can be out-of-sync, and can point in the middle of group import.
224+
// Let's move the pointer to the closest empty token (erased single import).
225+
if (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent()) {
226+
do {
227+
++$insertIndex;
228+
} while (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent());
229+
}
138230

139231
foreach ($statements as $index => $useDeclaration) {
140232
if ($this->areDeclarationsDifferent($currentUseDeclaration, $useDeclaration)) {

src/Tokenizer/Analyzer/Analysis/NamespaceUseAnalysis.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ final class NamespaceUseAnalysis implements StartEndTokenAwareAnalysis
6868

6969
/**
7070
* The type of import: class, function or constant.
71+
*
72+
* @var self::TYPE_*
7173
*/
7274
private int $type;
7375

@@ -140,6 +142,9 @@ public function getChunkEndIndex(): ?int
140142
return $this->chunkEndIndex;
141143
}
142144

145+
/**
146+
* @return self::TYPE_*
147+
*/
143148
public function getType(): int
144149
{
145150
return $this->type;

0 commit comments

Comments
 (0)