• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keradus / PHP-CS-Fixer / 17253322895

26 Aug 2025 11:52PM UTC coverage: 94.753% (+0.008%) from 94.745%
17253322895

push

github

keradus
add to git-blame-ignore-revs

28316 of 29884 relevant lines covered (94.75%)

45.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.6
/src/Fixer/Import/GroupImportFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Fixer\Import;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
27
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
28
use PhpCsFixer\Tokenizer\CT;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31
use PhpCsFixer\Utils;
32
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
33

34
/**
35
 * @phpstan-type _AutogeneratedInputConfiguration array{
36
 *  group_types?: list<string>,
37
 * }
38
 * @phpstan-type _AutogeneratedComputedConfiguration array{
39
 *  group_types: list<string>,
40
 * }
41
 *
42
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
43
 *
44
 * @author Volodymyr Kupriienko <vldmr.kuprienko@gmail.com>
45
 * @author Greg Korba <greg@codito.dev>
46
 *
47
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
48
 */
49
final class GroupImportFixer extends AbstractFixer implements ConfigurableFixerInterface
50
{
51
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
52
    use ConfigurableFixerTrait;
53

54
    /** @internal */
55
    public const GROUP_CLASSY = 'classy';
56

57
    /** @internal */
58
    public const GROUP_CONSTANTS = 'constants';
59

60
    /** @internal */
61
    public const GROUP_FUNCTIONS = 'functions';
62

63
    public function getDefinition(): FixerDefinitionInterface
64
    {
65
        return new FixerDefinition(
3✔
66
            'There MUST be group use for the same namespaces.',
3✔
67
            [
3✔
68
                new CodeSample(
3✔
69
                    "<?php\nuse Foo\\Bar;\nuse Foo\\Baz;\n"
3✔
70
                ),
3✔
71
                new CodeSample(
3✔
72
                    <<<'PHP'
3✔
73
                        <?php
74

75
                        use A\Foo;
76
                        use function B\foo;
77
                        use A\Bar;
78
                        use function B\bar;
79

80
                        PHP,
3✔
81
                    ['group_types' => [self::GROUP_CLASSY]]
3✔
82
                ),
3✔
83
            ]
3✔
84
        );
3✔
85
    }
86

87
    public function isCandidate(Tokens $tokens): bool
88
    {
89
        return $tokens->isTokenKindFound(\T_USE);
28✔
90
    }
91

92
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
93
    {
94
        $allowedTypes = [self::GROUP_CLASSY, self::GROUP_FUNCTIONS, self::GROUP_CONSTANTS];
37✔
95

96
        return new FixerConfigurationResolver([
37✔
97
            (new FixerOptionBuilder('group_types', 'Defines the order of import types.'))
37✔
98
                ->setAllowedTypes(['string[]'])
37✔
99
                ->setAllowedValues([static function (array $types) use ($allowedTypes): bool {
37✔
100
                    foreach ($types as $type) {
37✔
101
                        if (!\in_array($type, $allowedTypes, true)) {
37✔
102
                            throw new InvalidOptionsException(
×
103
                                \sprintf(
×
104
                                    'Invalid group type: %s, allowed types: %s.',
×
105
                                    $type,
×
106
                                    Utils::naturalLanguageJoin($allowedTypes)
×
107
                                )
×
108
                            );
×
109
                        }
110
                    }
111

112
                    return true;
37✔
113
                }])
37✔
114
                ->setDefault($allowedTypes)
37✔
115
                ->getOption(),
37✔
116
        ]);
37✔
117
    }
118

119
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
120
    {
121
        $useWithSameNamespaces = $this->getSameNamespacesByType($tokens);
28✔
122

123
        if ([] === $useWithSameNamespaces) {
28✔
124
            return;
21✔
125
        }
126

127
        $typeMap = [
28✔
128
            NamespaceUseAnalysis::TYPE_CLASS => self::GROUP_CLASSY,
28✔
129
            NamespaceUseAnalysis::TYPE_FUNCTION => self::GROUP_FUNCTIONS,
28✔
130
            NamespaceUseAnalysis::TYPE_CONSTANT => self::GROUP_CONSTANTS,
28✔
131
        ];
28✔
132

133
        // As a first step we need to remove all the use statements for the enabled import types.
134
        // We can't add new group imports yet, because we need to operate on previously determined token indices for all types.
135
        foreach ($useWithSameNamespaces as $type => $uses) {
28✔
136
            if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) {
28✔
137
                continue;
7✔
138
            }
139

140
            $this->removeSingleUseStatements($uses, $tokens);
28✔
141
        }
142

143
        foreach ($useWithSameNamespaces as $type => $uses) {
28✔
144
            if (!\in_array($typeMap[$type], $this->configuration['group_types'], true)) {
28✔
145
                continue;
7✔
146
            }
147

148
            $this->addGroupUseStatements($uses, $tokens);
28✔
149
        }
150
    }
151

152
    /**
153
     * Gets namespace use analyzers with same namespaces.
154
     *
155
     * @return array<NamespaceUseAnalysis::TYPE_*, non-empty-list<NamespaceUseAnalysis>>
156
     */
157
    private function getSameNamespacesByType(Tokens $tokens): array
158
    {
159
        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
28✔
160

161
        if (0 === \count($useDeclarations)) {
28✔
162
            return [];
10✔
163
        }
164

165
        $allNamespaceAndType = array_map(
28✔
166
            fn (NamespaceUseAnalysis $useDeclaration): string => $this->getNamespaceNameWithSlash($useDeclaration).$useDeclaration->getType(),
28✔
167
            $useDeclarations
28✔
168
        );
28✔
169

170
        $sameNamespaces = array_filter(array_count_values($allNamespaceAndType), static fn (int $count): bool => $count > 1);
28✔
171
        $sameNamespaces = array_keys($sameNamespaces);
28✔
172

173
        $sameNamespaceAnalysis = array_filter($useDeclarations, function (NamespaceUseAnalysis $useDeclaration) use ($sameNamespaces): bool {
28✔
174
            $namespaceNameAndType = $this->getNamespaceNameWithSlash($useDeclaration).$useDeclaration->getType();
28✔
175

176
            return \in_array($namespaceNameAndType, $sameNamespaces, true);
28✔
177
        });
28✔
178

179
        usort($sameNamespaceAnalysis, function (NamespaceUseAnalysis $a, NamespaceUseAnalysis $b): int {
28✔
180
            $namespaceA = $this->getNamespaceNameWithSlash($a);
28✔
181
            $namespaceB = $this->getNamespaceNameWithSlash($b);
28✔
182

183
            $namespaceDifference = \strlen($namespaceA) <=> \strlen($namespaceB);
28✔
184

185
            return 0 !== $namespaceDifference ? $namespaceDifference : $a->getFullName() <=> $b->getFullName();
28✔
186
        });
28✔
187

188
        $sameNamespaceAnalysisByType = [];
28✔
189
        foreach ($sameNamespaceAnalysis as $analysis) {
28✔
190
            $sameNamespaceAnalysisByType[$analysis->getType()][] = $analysis;
28✔
191
        }
192

193
        ksort($sameNamespaceAnalysisByType);
28✔
194

195
        return $sameNamespaceAnalysisByType;
28✔
196
    }
197

198
    /**
199
     * @param list<NamespaceUseAnalysis> $statements
200
     */
201
    private function removeSingleUseStatements(array $statements, Tokens $tokens): void
202
    {
203
        foreach ($statements as $useDeclaration) {
28✔
204
            $index = $useDeclaration->getStartIndex();
28✔
205
            $endIndex = $useDeclaration->getEndIndex();
28✔
206

207
            $useStatementTokens = [\T_USE, \T_WHITESPACE, \T_STRING, \T_NS_SEPARATOR, \T_AS, CT::T_CONST_IMPORT, CT::T_FUNCTION_IMPORT];
28✔
208

209
            while ($index !== $endIndex) {
28✔
210
                if ($tokens[$index]->isGivenKind($useStatementTokens)) {
28✔
211
                    $tokens->clearAt($index);
28✔
212
                }
213

214
                ++$index;
28✔
215
            }
216

217
            if (isset($tokens[$index]) && $tokens[$index]->equals(';')) {
28✔
218
                $tokens->clearAt($index);
28✔
219
            }
220

221
            ++$index;
28✔
222

223
            if (isset($tokens[$index]) && $tokens[$index]->isGivenKind(\T_WHITESPACE)) {
28✔
224
                $tokens->clearAt($index);
27✔
225
            }
226
        }
227
    }
228

229
    /**
230
     * @param list<NamespaceUseAnalysis> $statements
231
     */
232
    private function addGroupUseStatements(array $statements, Tokens $tokens): void
233
    {
234
        $currentUseDeclaration = null;
28✔
235
        $insertIndex = $statements[0]->getStartIndex();
28✔
236

237
        // If group import was inserted in place of removed imports, it may have more tokens than before,
238
        // and indices stored in imports of another type can be out-of-sync, and can point in the middle of group import.
239
        // Let's move the pointer to the closest empty token (erased single import).
240
        if (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent()) {
28✔
241
            do {
242
                ++$insertIndex;
3✔
243
            } while (null !== $tokens[$insertIndex]->getId() || '' !== $tokens[$insertIndex]->getContent());
3✔
244
        }
245

246
        foreach ($statements as $index => $useDeclaration) {
28✔
247
            if ($this->areDeclarationsDifferent($currentUseDeclaration, $useDeclaration)) {
28✔
248
                $currentUseDeclaration = $useDeclaration;
28✔
249
                $insertIndex += $this->createNewGroup(
28✔
250
                    $tokens,
28✔
251
                    $insertIndex,
28✔
252
                    $useDeclaration,
28✔
253
                    rtrim($this->getNamespaceNameWithSlash($currentUseDeclaration), '\\')
28✔
254
                );
28✔
255
            } else {
256
                $newTokens = [
28✔
257
                    new Token(','),
28✔
258
                    new Token([\T_WHITESPACE, ' ']),
28✔
259
                ];
28✔
260

261
                if ($useDeclaration->isAliased()) {
28✔
262
                    $tokens->insertAt($insertIndex, $newTokens);
4✔
263
                    $insertIndex += \count($newTokens);
4✔
264
                    $newTokens = [];
4✔
265

266
                    $insertIndex += $this->insertToGroupUseWithAlias($tokens, $insertIndex, $useDeclaration);
4✔
267
                }
268

269
                $newTokens[] = new Token([\T_STRING, $useDeclaration->getShortName()]);
28✔
270

271
                if (!isset($statements[$index + 1]) || $this->areDeclarationsDifferent($currentUseDeclaration, $statements[$index + 1])) {
28✔
272
                    $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']);
28✔
273
                    $newTokens[] = new Token(';');
28✔
274
                    $newTokens[] = new Token([\T_WHITESPACE, "\n"]);
28✔
275
                }
276

277
                $tokens->insertAt($insertIndex, $newTokens);
28✔
278
                $insertIndex += \count($newTokens);
28✔
279
            }
280
        }
281
    }
282

283
    private function getNamespaceNameWithSlash(NamespaceUseAnalysis $useDeclaration): string
284
    {
285
        $position = strrpos($useDeclaration->getFullName(), '\\');
28✔
286
        if (false === $position || 0 === $position) {
28✔
287
            return $useDeclaration->getFullName();
3✔
288
        }
289

290
        return substr($useDeclaration->getFullName(), 0, $position + 1);
28✔
291
    }
292

293
    /**
294
     * Insert use with alias to the group.
295
     */
296
    private function insertToGroupUseWithAlias(Tokens $tokens, int $insertIndex, NamespaceUseAnalysis $useDeclaration): int
297
    {
298
        $newTokens = [
6✔
299
            new Token([\T_STRING, substr($useDeclaration->getFullName(), strripos($useDeclaration->getFullName(), '\\') + 1)]),
6✔
300
            new Token([\T_WHITESPACE, ' ']),
6✔
301
            new Token([\T_AS, 'as']),
6✔
302
            new Token([\T_WHITESPACE, ' ']),
6✔
303
        ];
6✔
304

305
        $tokens->insertAt($insertIndex, $newTokens);
6✔
306

307
        return \count($newTokens);
6✔
308
    }
309

310
    /**
311
     * Creates new use statement group.
312
     */
313
    private function createNewGroup(Tokens $tokens, int $insertIndex, NamespaceUseAnalysis $useDeclaration, string $currentNamespace): int
314
    {
315
        $insertedTokens = 0;
28✔
316

317
        $newTokens = [
28✔
318
            new Token([\T_USE, 'use']),
28✔
319
            new Token([\T_WHITESPACE, ' ']),
28✔
320
        ];
28✔
321

322
        if ($useDeclaration->isFunction() || $useDeclaration->isConstant()) {
28✔
323
            $importStatementParams = $useDeclaration->isFunction()
8✔
324
                ? [CT::T_FUNCTION_IMPORT, 'function']
5✔
325
                : [CT::T_CONST_IMPORT, 'const'];
4✔
326

327
            $newTokens[] = new Token($importStatementParams);
8✔
328
            $newTokens[] = new Token([\T_WHITESPACE, ' ']);
8✔
329
        }
330

331
        $namespaceParts = explode('\\', $currentNamespace);
28✔
332

333
        foreach ($namespaceParts as $part) {
28✔
334
            $newTokens[] = new Token([\T_STRING, $part]);
28✔
335
            $newTokens[] = new Token([\T_NS_SEPARATOR, '\\']);
28✔
336
        }
337

338
        $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_OPEN, '{']);
28✔
339

340
        $newTokensCount = \count($newTokens);
28✔
341
        $tokens->insertAt($insertIndex, $newTokens);
28✔
342
        $insertedTokens += $newTokensCount;
28✔
343

344
        $insertIndex += $newTokensCount;
28✔
345

346
        if ($useDeclaration->isAliased()) {
28✔
347
            $inserted = $this->insertToGroupUseWithAlias($tokens, $insertIndex + 1, $useDeclaration) + 1;
4✔
348
            $insertedTokens += $inserted;
4✔
349
            $insertIndex += $inserted;
4✔
350
        }
351

352
        $tokens->insertAt($insertIndex, new Token([\T_STRING, $useDeclaration->getShortName()]));
28✔
353

354
        return ++$insertedTokens;
28✔
355
    }
356

357
    /**
358
     * Check if namespace use analyses are different.
359
     */
360
    private function areDeclarationsDifferent(?NamespaceUseAnalysis $analysis1, ?NamespaceUseAnalysis $analysis2): bool
361
    {
362
        if (null === $analysis1 || null === $analysis2) {
28✔
363
            return true;
28✔
364
        }
365

366
        $namespaceName1 = $this->getNamespaceNameWithSlash($analysis1);
28✔
367
        $namespaceName2 = $this->getNamespaceNameWithSlash($analysis2);
28✔
368

369
        return $namespaceName1 !== $namespaceName2 || $analysis1->getType() !== $analysis2->getType();
28✔
370
    }
371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc