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

keradus / PHP-CS-Fixer / 17678835382

12 Sep 2025 03:24PM UTC coverage: 94.69% (-0.06%) from 94.75%
17678835382

push

github

keradus
fix typo

1 of 1 new or added line in 1 file covered. (100.0%)

1042 existing lines in 177 files now uncovered.

28424 of 30018 relevant lines covered (94.69%)

45.5 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
                        <?php
62
                        use Foo, Sample, Sample\Sample as Sample2;
63

64
                        PHP
3✔
65
                ),
3✔
66
                new CodeSample(
3✔
67
                    <<<'PHP'
3✔
68
                        <?php
69
                        use Space\Models\ {
70
                            TestModelA,
71
                            TestModelB,
72
                            TestModel,
73
                        };
74

75
                        PHP,
3✔
76
                    ['group_to_single_imports' => true]
3✔
77
                ),
3✔
78
            ]
3✔
79
        );
3✔
80
    }
81

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

92
    public function isCandidate(Tokens $tokens): bool
93
    {
94
        return $tokens->isTokenKindFound(\T_USE);
20✔
95
    }
96

97
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
98
    {
99
        $tokensAnalyzer = new TokensAnalyzer($tokens);
20✔
100

101
        foreach (array_reverse($tokensAnalyzer->getImportUseIndexes()) as $index) {
20✔
102
            $endIndex = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
20✔
103
            $groupClose = $tokens->getPrevMeaningfulToken($endIndex);
20✔
104

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

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

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

134
        for ($i = $index + 1;; ++$i) {
7✔
135
            if ($tokens[$i]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_OPEN)) {
7✔
136
                $groupOpenIndex = $i;
7✔
137

138
                break;
7✔
139
            }
140

141
            if ($tokens[$i]->isComment()) {
7✔
UNCOV
142
                $comment .= $tokens[$i]->getContent();
×
UNCOV
143
                if (!$tokens[$i - 1]->isWhitespace() && !$tokens[$i + 1]->isWhitespace()) {
×
UNCOV
144
                    $groupPrefix .= ' ';
×
145
                }
146

UNCOV
147
                continue;
×
148
            }
149

150
            if ($tokens[$i]->isWhitespace()) {
7✔
151
                $groupPrefix .= ' ';
7✔
152

153
                continue;
7✔
154
            }
155

156
            $groupPrefix .= $tokens[$i]->getContent();
7✔
157
        }
158

159
        return [
7✔
160
            rtrim($groupPrefix),
7✔
161
            $groupOpenIndex,
7✔
162
            $tokens->findBlockEnd(Tokens::BLOCK_TYPE_GROUP_IMPORT_BRACE, $groupOpenIndex),
7✔
163
            $comment,
7✔
164
        ];
7✔
165
    }
166

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

175
        for ($i = $groupOpenIndex + 1; $i <= $groupCloseIndex; ++$i) {
7✔
176
            $token = $tokens[$i];
7✔
177

178
            if ($token->equals(',') && $tokens[$tokens->getNextMeaningfulToken($i)]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
7✔
179
                continue;
3✔
180
            }
181

182
            if ($token->equalsAny([',', [CT::T_GROUP_IMPORT_BRACE_CLOSE]])) {
7✔
183
                $statements[] = 'use'.$statement.';';
7✔
184
                $statement = $groupPrefix;
7✔
185

186
                continue;
7✔
187
            }
188

189
            if ($token->isWhitespace()) {
7✔
190
                $j = $tokens->getNextMeaningfulToken($i);
5✔
191

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

203
                if ($token->isWhitespace(" \t") || !str_starts_with($tokens[$i - 1]->getContent(), '//')) {
5✔
204
                    continue;
5✔
205
                }
206
            }
207

208
            $statement .= $token->getContent();
7✔
209
        }
210

211
        if ('' !== $comment) {
7✔
UNCOV
212
            $statements[0] .= ' '.$comment;
×
213
        }
214

215
        return $statements;
7✔
216
    }
217

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

223
        $tokens->clearRange($index, $groupCloseIndex);
7✔
224
        if ($tokens[$endIndex]->equals(';')) {
7✔
225
            $tokens->clearAt($endIndex);
7✔
226
        }
227

228
        $ending = $this->whitespacesConfig->getLineEnding();
7✔
229
        $importTokens = Tokens::fromCode('<?php '.implode($ending, $statements));
7✔
230
        $importTokens->clearAt(0);
7✔
231
        $importTokens->clearEmptyTokens();
7✔
232

233
        $tokens->insertAt($index, $importTokens);
7✔
234
    }
235

236
    private function fixMultipleUse(Tokens $tokens, int $index, int $endIndex): void
237
    {
238
        $nextTokenIndex = $tokens->getNextMeaningfulToken($index);
19✔
239

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

254
        $ending = $this->whitespacesConfig->getLineEnding();
19✔
255

256
        for ($i = $endIndex - 1; $i > $index; --$i) {
19✔
257
            if (!$tokens[$i]->equals(',')) {
19✔
258
                continue;
19✔
259
            }
260

261
            $tokens[$i] = new Token(';');
13✔
262
            $i = $tokens->getNextMeaningfulToken($i);
13✔
263

264
            $tokens->insertAt($i, new Token([\T_USE, 'use']));
13✔
265
            $tokens->insertAt($i + 1, new Token([\T_WHITESPACE, ' ']));
13✔
266

267
            foreach ($leadingTokens as $offset => $leadingToken) {
13✔
268
                $tokens->insertAt($i + 2 + $offset, clone $leadingToken);
1✔
269
            }
270

271
            $indent = WhitespacesAnalyzer::detectIndent($tokens, $index);
13✔
272

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