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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 hits per line

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

95.69
/src/Fixer/Import/SingleImportPerStatementFixer.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\Fixer\WhitespacesAwareFixerInterface;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\CodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
28
use PhpCsFixer\Tokenizer\CT;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31
use PhpCsFixer\Tokenizer\TokensAnalyzer;
32

33
/**
34
 * Fixer for rules defined in PSR2 ¶3.
35
 *
36
 * @phpstan-type _AutogeneratedInputConfiguration array{
37
 *  group_to_single_imports?: bool,
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  group_to_single_imports: bool,
41
 * }
42
 *
43
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
44
 *
45
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
46
 *
47
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
48
 */
49
final class SingleImportPerStatementFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
50
{
51
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
52
    use ConfigurableFixerTrait;
53

54
    public function getDefinition(): FixerDefinitionInterface
55
    {
56
        return new FixerDefinition(
3✔
57
            'There MUST be one use keyword per declaration.',
3✔
58
            [
3✔
59
                new CodeSample(
3✔
60
                    '<?php
3✔
61
use Foo, Sample, Sample\Sample as Sample2;
62
'
3✔
63
                ),
3✔
64
                new CodeSample(
3✔
65
                    '<?php
3✔
66
use Space\Models\ {
67
    TestModelA,
68
    TestModelB,
69
    TestModel,
70
};
71
',
3✔
72
                    ['group_to_single_imports' => true]
3✔
73
                ),
3✔
74
            ]
3✔
75
        );
3✔
76
    }
77

78
    /**
79
     * {@inheritdoc}
80
     *
81
     * Must run before MultilineWhitespaceBeforeSemicolonsFixer, NoLeadingImportSlashFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer, SpaceAfterSemicolonFixer.
82
     */
83
    public function getPriority(): int
84
    {
85
        return 1;
1✔
86
    }
87

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

93
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
94
    {
95
        $tokensAnalyzer = new TokensAnalyzer($tokens);
20✔
96

97
        foreach (array_reverse($tokensAnalyzer->getImportUseIndexes()) as $index) {
20✔
98
            $endIndex = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
20✔
99
            $groupClose = $tokens->getPrevMeaningfulToken($endIndex);
20✔
100

101
            if ($tokens[$groupClose]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
20✔
102
                if (true === $this->configuration['group_to_single_imports']) {
8✔
103
                    $this->fixGroupUse($tokens, $index, $endIndex);
7✔
104
                }
105
            } else {
106
                $this->fixMultipleUse($tokens, $index, $endIndex);
19✔
107
            }
108
        }
109
    }
110

111
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
112
    {
113
        return new FixerConfigurationResolver([
29✔
114
            (new FixerOptionBuilder('group_to_single_imports', 'Whether to change group imports into single imports.'))
29✔
115
                ->setAllowedTypes(['bool'])
29✔
116
                ->setDefault(true)
29✔
117
                ->getOption(),
29✔
118
        ]);
29✔
119
    }
120

121
    /**
122
     * @return array{string, ?int, int, string}
123
     */
124
    private function getGroupDeclaration(Tokens $tokens, int $index): array
125
    {
126
        $groupPrefix = '';
7✔
127
        $comment = '';
7✔
128
        $groupOpenIndex = null;
7✔
129

130
        for ($i = $index + 1;; ++$i) {
7✔
131
            if ($tokens[$i]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_OPEN)) {
7✔
132
                $groupOpenIndex = $i;
7✔
133

134
                break;
7✔
135
            }
136

137
            if ($tokens[$i]->isComment()) {
7✔
138
                $comment .= $tokens[$i]->getContent();
×
139
                if (!$tokens[$i - 1]->isWhitespace() && !$tokens[$i + 1]->isWhitespace()) {
×
140
                    $groupPrefix .= ' ';
×
141
                }
142

143
                continue;
×
144
            }
145

146
            if ($tokens[$i]->isWhitespace()) {
7✔
147
                $groupPrefix .= ' ';
7✔
148

149
                continue;
7✔
150
            }
151

152
            $groupPrefix .= $tokens[$i]->getContent();
7✔
153
        }
154

155
        return [
7✔
156
            rtrim($groupPrefix),
7✔
157
            $groupOpenIndex,
7✔
158
            $tokens->findBlockEnd(Tokens::BLOCK_TYPE_GROUP_IMPORT_BRACE, $groupOpenIndex),
7✔
159
            $comment,
7✔
160
        ];
7✔
161
    }
162

163
    /**
164
     * @return list<string>
165
     */
166
    private function getGroupStatements(Tokens $tokens, string $groupPrefix, int $groupOpenIndex, int $groupCloseIndex, string $comment): array
167
    {
168
        $statements = [];
7✔
169
        $statement = $groupPrefix;
7✔
170

171
        for ($i = $groupOpenIndex + 1; $i <= $groupCloseIndex; ++$i) {
7✔
172
            $token = $tokens[$i];
7✔
173

174
            if ($token->equals(',') && $tokens[$tokens->getNextMeaningfulToken($i)]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
7✔
175
                continue;
3✔
176
            }
177

178
            if ($token->equalsAny([',', [CT::T_GROUP_IMPORT_BRACE_CLOSE]])) {
7✔
179
                $statements[] = 'use'.$statement.';';
7✔
180
                $statement = $groupPrefix;
7✔
181

182
                continue;
7✔
183
            }
184

185
            if ($token->isWhitespace()) {
7✔
186
                $j = $tokens->getNextMeaningfulToken($i);
5✔
187

188
                if ($tokens[$j]->isGivenKind(\T_AS)) {
5✔
189
                    $statement .= ' as ';
1✔
190
                    $i += 2;
1✔
191
                } elseif ($tokens[$j]->isGivenKind(CT::T_FUNCTION_IMPORT)) {
5✔
192
                    $statement = ' function'.$statement;
1✔
193
                    $i += 2;
1✔
194
                } elseif ($tokens[$j]->isGivenKind(CT::T_CONST_IMPORT)) {
5✔
195
                    $statement = ' const'.$statement;
1✔
196
                    $i += 2;
1✔
197
                }
198

199
                if ($token->isWhitespace(" \t") || !str_starts_with($tokens[$i - 1]->getContent(), '//')) {
5✔
200
                    continue;
5✔
201
                }
202
            }
203

204
            $statement .= $token->getContent();
7✔
205
        }
206

207
        if ('' !== $comment) {
7✔
208
            $statements[0] .= ' '.$comment;
×
209
        }
210

211
        return $statements;
7✔
212
    }
213

214
    private function fixGroupUse(Tokens $tokens, int $index, int $endIndex): void
215
    {
216
        [$groupPrefix, $groupOpenIndex, $groupCloseIndex, $comment] = $this->getGroupDeclaration($tokens, $index);
7✔
217
        $statements = $this->getGroupStatements($tokens, $groupPrefix, $groupOpenIndex, $groupCloseIndex, $comment);
7✔
218

219
        $tokens->clearRange($index, $groupCloseIndex);
7✔
220
        if ($tokens[$endIndex]->equals(';')) {
7✔
221
            $tokens->clearAt($endIndex);
7✔
222
        }
223

224
        $ending = $this->whitespacesConfig->getLineEnding();
7✔
225
        $importTokens = Tokens::fromCode('<?php '.implode($ending, $statements));
7✔
226
        $importTokens->clearAt(0);
7✔
227
        $importTokens->clearEmptyTokens();
7✔
228

229
        $tokens->insertAt($index, $importTokens);
7✔
230
    }
231

232
    private function fixMultipleUse(Tokens $tokens, int $index, int $endIndex): void
233
    {
234
        $nextTokenIndex = $tokens->getNextMeaningfulToken($index);
19✔
235

236
        if ($tokens[$nextTokenIndex]->isGivenKind(CT::T_FUNCTION_IMPORT)) {
19✔
237
            $leadingTokens = [
2✔
238
                new Token([CT::T_FUNCTION_IMPORT, 'function']),
2✔
239
                new Token([\T_WHITESPACE, ' ']),
2✔
240
            ];
2✔
241
        } elseif ($tokens[$nextTokenIndex]->isGivenKind(CT::T_CONST_IMPORT)) {
19✔
242
            $leadingTokens = [
2✔
243
                new Token([CT::T_CONST_IMPORT, 'const']),
2✔
244
                new Token([\T_WHITESPACE, ' ']),
2✔
245
            ];
2✔
246
        } else {
247
            $leadingTokens = [];
19✔
248
        }
249

250
        $ending = $this->whitespacesConfig->getLineEnding();
19✔
251

252
        for ($i = $endIndex - 1; $i > $index; --$i) {
19✔
253
            if (!$tokens[$i]->equals(',')) {
19✔
254
                continue;
19✔
255
            }
256

257
            $tokens[$i] = new Token(';');
13✔
258
            $i = $tokens->getNextMeaningfulToken($i);
13✔
259

260
            $tokens->insertAt($i, new Token([\T_USE, 'use']));
13✔
261
            $tokens->insertAt($i + 1, new Token([\T_WHITESPACE, ' ']));
13✔
262

263
            foreach ($leadingTokens as $offset => $leadingToken) {
13✔
264
                $tokens->insertAt($i + 2 + $offset, clone $leadingToken);
1✔
265
            }
266

267
            $indent = WhitespacesAnalyzer::detectIndent($tokens, $index);
13✔
268

269
            if ($tokens[$i - 1]->isWhitespace()) {
13✔
270
                $tokens[$i - 1] = new Token([\T_WHITESPACE, $ending.$indent]);
10✔
271
            } elseif (!str_contains($tokens[$i - 1]->getContent(), "\n")) {
5✔
272
                $tokens->insertAt($i, new Token([\T_WHITESPACE, $ending.$indent]));
4✔
273
            }
274
        }
275
    }
276
}
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