NoUnusedImportsFixer::removeUseDeclaration()   F
last analyzed

Complexity

Conditions 16
Paths 330

Size

Total Lines 82
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 46
c 1
b 0
f 0
dl 0
loc 82
rs 3.1083
cc 16
nc 330
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 *     Dariusz Rumiński <[email protected]>
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\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Preg;
22
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
23
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
24
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
25
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
26
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Tokenizer\TokensAnalyzer;
30
31
/**
32
 * @author Dariusz Rumiński <[email protected]>
33
 */
34
final class NoUnusedImportsFixer extends AbstractFixer
35
{
36
    /**
37
     * {@inheritdoc}
38
     */
39
    public function getDefinition(): FixerDefinitionInterface
40
    {
41
        return new FixerDefinition(
42
            'Unused `use` statements must be removed.',
43
            [new CodeSample("<?php\nuse \\DateTime;\nuse \\Exception;\n\nnew DateTime();\n")]
44
        );
45
    }
46
47
    /**
48
     * {@inheritdoc}
49
     *
50
     * Must run before BlankLineAfterNamespaceFixer, NoExtraBlankLinesFixer, NoLeadingImportSlashFixer, SingleLineAfterImportsFixer.
51
     * Must run after ClassKeywordRemoveFixer, GlobalNamespaceImportFixer, PhpUnitFqcnAnnotationFixer, SingleImportPerStatementFixer.
52
     */
53
    public function getPriority(): int
54
    {
55
        return -10;
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function isCandidate(Tokens $tokens): bool
62
    {
63
        return $tokens->isTokenKindFound(T_USE);
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
70
    {
71
        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
72
73
        if (0 === \count($useDeclarations)) {
74
            return;
75
        }
76
77
        foreach ((new NamespacesAnalyzer())->getDeclarations($tokens) as $namespace) {
78
            $currentNamespaceUseDeclarations = [];
79
            $currentNamespaceUseDeclarationIndexes = [];
80
81
            foreach ($useDeclarations as $useDeclaration) {
82
                if ($useDeclaration->getStartIndex() >= $namespace->getScopeStartIndex() && $useDeclaration->getEndIndex() <= $namespace->getScopeEndIndex()) {
83
                    $currentNamespaceUseDeclarations[] = $useDeclaration;
84
                    $currentNamespaceUseDeclarationIndexes[$useDeclaration->getStartIndex()] = $useDeclaration->getEndIndex();
85
                }
86
            }
87
88
            foreach ($currentNamespaceUseDeclarations as $useDeclaration) {
89
                if (!$this->isImportUsed($tokens, $namespace, $useDeclaration, $currentNamespaceUseDeclarationIndexes)) {
90
                    $this->removeUseDeclaration($tokens, $useDeclaration);
91
                }
92
            }
93
94
            $this->removeUsesInSameNamespace($tokens, $currentNamespaceUseDeclarations, $namespace);
95
        }
96
    }
97
98
    /**
99
     * @param array<int, int> $ignoredIndexes indexes of the use statements themselves that should not be checked as being "used"
100
     */
101
    private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, NamespaceUseAnalysis $import, array $ignoredIndexes): bool
102
    {
103
        $analyzer = new TokensAnalyzer($tokens);
104
        $gotoLabelAnalyzer = new GotoLabelAnalyzer();
105
106
        $tokensNotBeforeFunctionCall = [T_NEW];
107
        // @TODO: drop condition when PHP 8.0+ is required
108
        if (\defined('T_ATTRIBUTE')) {
109
            $tokensNotBeforeFunctionCall[] = T_ATTRIBUTE;
110
        }
111
112
        $namespaceEndIndex = $namespace->getScopeEndIndex();
113
        for ($index = $namespace->getScopeStartIndex(); $index <= $namespaceEndIndex; ++$index) {
114
            if (isset($ignoredIndexes[$index])) {
115
                $index = $ignoredIndexes[$index];
116
117
                continue;
118
            }
119
120
            $token = $tokens[$index];
121
122
            if ($token->isGivenKind(T_STRING)) {
123
                if (0 !== strcasecmp($import->getShortName(), $token->getContent())) {
124
                    continue;
125
                }
126
127
                $prevMeaningfulToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
128
129
                if ($prevMeaningfulToken->isGivenKind(T_NAMESPACE)) {
130
                    $index = $tokens->getNextTokenOfKind($index, [';', '{', [T_CLOSE_TAG]]);
131
132
                    continue;
133
                }
134
135
                if (
136
                    $prevMeaningfulToken->isGivenKind([T_NS_SEPARATOR, T_FUNCTION, T_CONST, T_DOUBLE_COLON])
137
                    || $prevMeaningfulToken->isObjectOperator()
138
                ) {
139
                    continue;
140
                }
141
142
                $nextMeaningfulIndex = $tokens->getNextMeaningfulToken($index);
143
144
                if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, $nextMeaningfulIndex)) {
0 ignored issues
show
Bug introduced by
It seems like $nextMeaningfulIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Ana...r::belongsToGoToLabel() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

144
                if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, /** @scrutinizer ignore-type */ $nextMeaningfulIndex)) {
Loading history...
145
                    continue;
146
                }
147
148
                $nextMeaningfulToken = $tokens[$nextMeaningfulIndex];
149
150
                if ($analyzer->isConstantInvocation($index)) {
151
                    $type = NamespaceUseAnalysis::TYPE_CONSTANT;
152
                } elseif ($nextMeaningfulToken->equals('(') && !$prevMeaningfulToken->isGivenKind($tokensNotBeforeFunctionCall)) {
153
                    $type = NamespaceUseAnalysis::TYPE_FUNCTION;
154
                } else {
155
                    $type = NamespaceUseAnalysis::TYPE_CLASS;
156
                }
157
158
                if ($import->getType() === $type) {
159
                    return true;
160
                }
161
162
                continue;
163
            }
164
165
            if ($token->isComment()
166
                && Preg::match(
167
                    '/(?<![[:alnum:]\$])(?<!\\\\)'.$import->getShortName().'(?![[:alnum:]])/i',
168
                    $token->getContent()
169
                )
170
            ) {
171
                return true;
172
            }
173
        }
174
175
        return false;
176
    }
177
178
    private function removeUseDeclaration(Tokens $tokens, NamespaceUseAnalysis $useDeclaration): void
179
    {
180
        for ($index = $useDeclaration->getEndIndex() - 1; $index >= $useDeclaration->getStartIndex(); --$index) {
181
            if ($tokens[$index]->isComment()) {
182
                continue;
183
            }
184
185
            if (!$tokens[$index]->isWhitespace() || false === strpos($tokens[$index]->getContent(), "\n")) {
186
                $tokens->clearTokenAndMergeSurroundingWhitespace($index);
187
188
                continue;
189
            }
190
191
            // when multi line white space keep the line feed if the previous token is a comment
192
            $prevIndex = $tokens->getPrevNonWhitespace($index);
193
            if ($tokens[$prevIndex]->isComment()) {
194
                $content = $tokens[$index]->getContent();
195
                $tokens[$index] = new Token([T_WHITESPACE, substr($content, strrpos($content, "\n"))]); // preserve indent only
196
            } else {
197
                $tokens->clearTokenAndMergeSurroundingWhitespace($index);
198
            }
199
        }
200
201
        if ($tokens[$useDeclaration->getEndIndex()]->equals(';')) { // do not remove `? >`
202
            $tokens->clearAt($useDeclaration->getEndIndex());
203
        }
204
205
        // remove white space above and below where the `use` statement was
206
207
        $prevIndex = $useDeclaration->getStartIndex() - 1;
208
        $prevToken = $tokens[$prevIndex];
209
210
        if ($prevToken->isWhitespace()) {
211
            $content = rtrim($prevToken->getContent(), " \t");
212
213
            if ('' === $content) {
214
                $tokens->clearAt($prevIndex);
215
            } else {
216
                $tokens[$prevIndex] = new Token([T_WHITESPACE, $content]);
217
            }
218
219
            $prevToken = $tokens[$prevIndex];
220
        }
221
222
        if (!isset($tokens[$useDeclaration->getEndIndex() + 1])) {
223
            return;
224
        }
225
226
        $nextIndex = $tokens->getNonEmptySibling($useDeclaration->getEndIndex(), 1);
227
        if (null === $nextIndex) {
228
            return;
229
        }
230
231
        $nextToken = $tokens[$nextIndex];
232
233
        if ($nextToken->isWhitespace()) {
234
            $content = Preg::replace(
235
                "#^\r\n|^\n#",
236
                '',
237
                ltrim($nextToken->getContent(), " \t"),
238
                1
239
            );
240
241
            if ('' !== $content) {
242
                $tokens[$nextIndex] = new Token([T_WHITESPACE, $content]);
243
            } else {
244
                $tokens->clearAt($nextIndex);
245
            }
246
247
            $nextToken = $tokens[$nextIndex];
248
        }
249
250
        if ($prevToken->isWhitespace() && $nextToken->isWhitespace()) {
251
            $content = $prevToken->getContent().$nextToken->getContent();
252
253
            if ('' !== $content) {
254
                $tokens[$nextIndex] = new Token([T_WHITESPACE, $content]);
255
            } else {
256
                $tokens->clearAt($nextIndex);
257
            }
258
259
            $tokens->clearAt($prevIndex);
260
        }
261
    }
262
263
    private function removeUsesInSameNamespace(Tokens $tokens, array $useDeclarations, NamespaceAnalysis $namespaceDeclaration): void
264
    {
265
        $namespace = $namespaceDeclaration->getFullName();
266
        $nsLength = \strlen($namespace.'\\');
267
268
        foreach ($useDeclarations as $useDeclaration) {
269
            if ($useDeclaration->isAliased()) {
270
                continue;
271
            }
272
273
            $useDeclarationFullName = ltrim($useDeclaration->getFullName(), '\\');
274
275
            if (0 !== strpos($useDeclarationFullName, $namespace.'\\')) {
276
                continue;
277
            }
278
279
            $partName = substr($useDeclarationFullName, $nsLength);
280
281
            if (false === strpos($partName, '\\')) {
282
                $this->removeUseDeclaration($tokens, $useDeclaration);
283
            }
284
        }
285
    }
286
}
287