15
15
namespace PhpCsFixer \Fixer \Import ;
16
16
17
17
use PhpCsFixer \AbstractFixer ;
18
+ use PhpCsFixer \Fixer \ConfigurableFixerInterface ;
19
+ use PhpCsFixer \FixerConfiguration \FixerConfigurationResolver ;
20
+ use PhpCsFixer \FixerConfiguration \FixerConfigurationResolverInterface ;
21
+ use PhpCsFixer \FixerConfiguration \FixerOptionBuilder ;
18
22
use PhpCsFixer \FixerDefinition \CodeSample ;
19
23
use PhpCsFixer \FixerDefinition \FixerDefinition ;
20
24
use PhpCsFixer \FixerDefinition \FixerDefinitionInterface ;
23
27
use PhpCsFixer \Tokenizer \CT ;
24
28
use PhpCsFixer \Tokenizer \Token ;
25
29
use PhpCsFixer \Tokenizer \Tokens ;
30
+ use PhpCsFixer \Utils ;
31
+ use Symfony \Component \OptionsResolver \Exception \InvalidOptionsException ;
26
32
27
33
/**
28
34
* @author Volodymyr Kupriienko <[email protected] >
35
+ * @author Greg Korba <[email protected] >
29
36
*/
30
- final class GroupImportFixer extends AbstractFixer
37
+ final class GroupImportFixer extends AbstractFixer implements ConfigurableFixerInterface
31
38
{
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
+
32
48
public function getDefinition (): FixerDefinitionInterface
33
49
{
34
50
return new FixerDefinition (
@@ -37,6 +53,18 @@ public function getDefinition(): FixerDefinitionInterface
37
53
new CodeSample (
38
54
"<?php \nuse Foo \\Bar; \nuse Foo \\Baz; \n"
39
55
),
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
+ ),
40
68
]
41
69
);
42
70
}
@@ -46,24 +74,72 @@ public function isCandidate(Tokens $tokens): bool
46
74
return $ tokens ->isTokenKindFound (T_USE );
47
75
}
48
76
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
+
49
104
protected function applyFix (\SplFileInfo $ file , Tokens $ tokens ): void
50
105
{
51
- $ useWithSameNamespaces = $ this ->getSameNamespaces ($ tokens );
106
+ $ useWithSameNamespaces = $ this ->getSameNamespacesByType ($ tokens );
52
107
53
108
if ([] === $ useWithSameNamespaces ) {
54
109
return ;
55
110
}
56
111
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
+ }
59
135
}
60
136
61
137
/**
62
138
* Gets namespace use analyzers with same namespaces.
63
139
*
64
- * @return list<NamespaceUseAnalysis>
140
+ * @return array<NamespaceUseAnalysis::TYPE_*, list<NamespaceUseAnalysis> >
65
141
*/
66
- private function getSameNamespaces (Tokens $ tokens ): array
142
+ private function getSameNamespacesByType (Tokens $ tokens ): array
67
143
{
68
144
$ useDeclarations = (new NamespaceUsesAnalyzer ())->getDeclarationsFromTokens ($ tokens );
69
145
@@ -94,7 +170,14 @@ private function getSameNamespaces(Tokens $tokens): array
94
170
return 0 !== $ namespaceDifference ? $ namespaceDifference : $ a ->getFullName () <=> $ b ->getFullName ();
95
171
});
96
172
97
- return $ sameNamespaceAnalysis ;
173
+ $ sameNamespaceAnalysisByType = [];
174
+ foreach ($ sameNamespaceAnalysis as $ analysis ) {
175
+ $ sameNamespaceAnalysisByType [$ analysis ->getType ()][] = $ analysis ;
176
+ }
177
+
178
+ ksort ($ sameNamespaceAnalysisByType );
179
+
180
+ return $ sameNamespaceAnalysisByType ;
98
181
}
99
182
100
183
/**
@@ -134,7 +217,16 @@ private function removeSingleUseStatements(array $statements, Tokens $tokens): v
134
217
private function addGroupUseStatements (array $ statements , Tokens $ tokens ): void
135
218
{
136
219
$ 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
+ }
138
230
139
231
foreach ($ statements as $ index => $ useDeclaration ) {
140
232
if ($ this ->areDeclarationsDifferent ($ currentUseDeclaration , $ useDeclaration )) {
0 commit comments