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

webimpress / coding-standard / 4086684577

pending completion
4086684577

Pull #178

github

GitHub
Merge 75aa3b533 into 18aa29088
Pull Request #178: Bump phpunit/phpunit from 9.5.20 to 9.6.0

6985 of 6999 relevant lines covered (99.8%)

1.13 hits per line

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

99.64
/src/WebimpressCodingStandard/Sniffs/Namespaces/UnusedUseStatementSniff.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace WebimpressCodingStandard\Sniffs\Namespaces;
6

7
use PHP_CodeSniffer\Files\File;
8
use PHP_CodeSniffer\Sniffs\Sniff;
9
use PHP_CodeSniffer\Util\Tokens;
10
use WebimpressCodingStandard\CodingStandard;
11

12
use function in_array;
13
use function preg_match;
14
use function preg_quote;
15
use function strcasecmp;
16
use function strrpos;
17
use function strtolower;
18
use function substr;
19
use function trim;
20

21
use const T_AS;
22
use const T_BITWISE_OR;
23
use const T_CATCH;
24
use const T_CLOSE_PARENTHESIS;
25
use const T_CLOSE_USE_GROUP;
26
use const T_COLON;
27
use const T_COMMA;
28
use const T_CONST;
29
use const T_CURLY_OPEN;
30
use const T_DOC_COMMENT_STRING;
31
use const T_DOC_COMMENT_TAG;
32
use const T_DOUBLE_COLON;
33
use const T_ELLIPSIS;
34
use const T_EXTENDS;
35
use const T_FUNCTION;
36
use const T_IMPLEMENTS;
37
use const T_INSTANCEOF;
38
use const T_INSTEADOF;
39
use const T_NAMESPACE;
40
use const T_NEW;
41
use const T_NS_SEPARATOR;
42
use const T_NULLABLE;
43
use const T_OBJECT_OPERATOR;
44
use const T_OPEN_CURLY_BRACKET;
45
use const T_OPEN_PARENTHESIS;
46
use const T_OPEN_USE_GROUP;
47
use const T_SEMICOLON;
48
use const T_STRING;
49
use const T_USE;
50
use const T_VARIABLE;
51
use const T_WHITESPACE;
52

53
/**
54
 * Below class is mixture of:
55
 *
56
 * @see http://jdon.at/1h0wb
57
 * @see https://github.com/squizlabs/PHP_CodeSniffer/pull/1106
58
 *     - added checks in annotations
59
 *     - added checks in return type (PHP 7.0+)
60
 *     - remove unused use statements in files without namespace
61
 *     - support for grouped use declarations
62
 */
63
class UnusedUseStatementSniff implements Sniff
64
{
65
    /**
66
     * @var int[]
67
     */
68
    private $checkInTokens = [
69
        T_STRING,
70
        T_DOC_COMMENT_STRING,
71
        T_DOC_COMMENT_TAG,
72
    ];
73

74
    /**
75
     * @return int[]
76
     */
77
    public function register() : array
78
    {
79
        return [T_USE];
1✔
80
    }
81

82
    /**
83
     * @param int $stackPtr
84
     */
85
    public function process(File $phpcsFile, $stackPtr)
86
    {
87
        // Only check use statements in the global scope.
88
        if (! CodingStandard::isGlobalUse($phpcsFile, $stackPtr)) {
1✔
89
            return;
1✔
90
        }
91

92
        $tokens = $phpcsFile->getTokens();
1✔
93

94
        $semiColon = $phpcsFile->findEndOfStatement($stackPtr);
1✔
95
        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $semiColon - 1, null, true);
1✔
96

97
        if ($tokens[$prev]['code'] === T_CLOSE_USE_GROUP) {
1✔
98
            $to = $prev;
1✔
99
            $from = $phpcsFile->findPrevious(T_OPEN_USE_GROUP, $prev - 1);
1✔
100

101
            // Empty group is invalid syntax
102
            if ($phpcsFile->findNext(Tokens::$emptyTokens, $from + 1, null, true) === $to) {
1✔
103
                $error = 'Empty use group';
1✔
104

105
                $fix = $phpcsFile->addFixableError($error, $stackPtr, 'EmptyUseGroup');
1✔
106
                if ($fix) {
1✔
107
                    $this->removeUse($phpcsFile, $stackPtr, $semiColon);
1✔
108
                }
109

110
                return;
1✔
111
            }
112

113
            $comma = $phpcsFile->findNext(T_COMMA, $from + 1, $to);
1✔
114
            if ($comma === false
1✔
115
                || $phpcsFile->findNext(Tokens::$emptyTokens, $comma + 1, $to, true) === false
1✔
116
            ) {
117
                $error = 'Redundant use group for one declaration';
1✔
118

119
                $fix = $phpcsFile->addFixableError($error, $stackPtr, 'RedundantUseGroup');
1✔
120
                if ($fix) {
1✔
121
                    $phpcsFile->fixer->beginChangeset();
1✔
122
                    $phpcsFile->fixer->replaceToken($from, '');
1✔
123
                    $i = $from + 1;
1✔
124
                    while ($tokens[$i]['code'] === T_WHITESPACE) {
1✔
125
                        $phpcsFile->fixer->replaceToken($i, '');
1✔
126
                        ++$i;
1✔
127
                    }
128

129
                    if ($comma !== false) {
1✔
130
                        $phpcsFile->fixer->replaceToken($comma, '');
1✔
131
                    }
132

133
                    $phpcsFile->fixer->replaceToken($to, '');
1✔
134
                    $i = $to - 1;
1✔
135
                    while ($tokens[$i]['code'] === T_WHITESPACE) {
1✔
136
                        $phpcsFile->fixer->replaceToken($i, '');
1✔
137
                        --$i;
1✔
138
                    }
139
                    $phpcsFile->fixer->endChangeset();
1✔
140
                }
141

142
                return;
1✔
143
            }
144

145
            $skip = Tokens::$emptyTokens + [T_COMMA => T_COMMA];
1✔
146

147
            while ($classPtr = $phpcsFile->findPrevious($skip, $to - 1, $from + 1, true)) {
1✔
148
                $to = $phpcsFile->findPrevious(T_COMMA, $classPtr - 1, $from + 1);
1✔
149

150
                if (! $this->isClassUsed($phpcsFile, $stackPtr, $classPtr)) {
1✔
151
                    $error = 'Unused use statement "%s"';
1✔
152
                    $data = [$tokens[$classPtr]['content']];
1✔
153

154
                    $fix = $phpcsFile->addFixableError($error, $classPtr, 'UnusedUseInGroup', $data);
1✔
155
                    if ($fix) {
1✔
156
                        $first = $to === false ? $from + 1 : $to;
1✔
157
                        $last = $classPtr;
1✔
158
                        if ($to === false) {
1✔
159
                            $next = $phpcsFile->findNext(Tokens::$emptyTokens, $classPtr + 1, null, true);
1✔
160
                            if ($tokens[$next]['code'] === T_COMMA) {
1✔
161
                                $last = $next;
1✔
162
                            }
163
                        }
164

165
                        $phpcsFile->fixer->beginChangeset();
1✔
166
                        for ($i = $first; $i <= $last; ++$i) {
1✔
167
                            $phpcsFile->fixer->replaceToken($i, '');
1✔
168
                        }
169
                        $phpcsFile->fixer->endChangeset();
1✔
170
                    }
171
                }
172

173
                if ($to === false) {
1✔
174
                    break;
1✔
175
                }
176
            }
177

178
            return;
1✔
179
        }
180

181
        do {
182
            $classPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $semiColon - 1, null, true);
1✔
183
            if (! $this->isClassUsed($phpcsFile, $stackPtr, $classPtr)) {
1✔
184
                $warning = 'Unused use statement "%s"';
1✔
185
                $data = [$tokens[$classPtr]['content']];
1✔
186
                $fix = $phpcsFile->addFixableError($warning, $stackPtr, 'UnusedUse', $data);
1✔
187

188
                if ($fix) {
1✔
189
                    $prev = $phpcsFile->findPrevious(
1✔
190
                        Tokens::$emptyTokens + [
1✔
191
                            T_STRING => T_STRING,
1✔
192
                            T_NS_SEPARATOR => T_NS_SEPARATOR,
1✔
193
                            T_AS => T_AS,
1✔
194
                        ],
1✔
195
                        $classPtr,
1✔
196
                        null,
1✔
197
                        true
1✔
198
                    );
1✔
199

200
                    $to = $semiColon;
1✔
201
                    if ($tokens[$prev]['code'] === T_COMMA) {
1✔
202
                        $from = $prev;
1✔
203
                        $to = $classPtr;
1✔
204
                    } elseif ($tokens[$semiColon]['code'] === T_SEMICOLON) {
1✔
205
                        $from = $stackPtr;
1✔
206
                    } else {
207
                        $from = $phpcsFile->findNext(Tokens::$emptyTokens, $prev + 1, null, true);
1✔
208
                        if ($tokens[$from]['code'] === T_STRING
1✔
209
                            && in_array(strtolower($tokens[$from]['content']), ['const', 'function'], true)
1✔
210
                        ) {
211
                            $from = $phpcsFile->findNext(Tokens::$emptyTokens, $from + 1, null, true);
1✔
212
                        }
213
                    }
214

215
                    $this->removeUse($phpcsFile, $from, $to);
1✔
216
                }
217
            }
218

219
            if ($tokens[$semiColon]['code'] === T_SEMICOLON) {
1✔
220
                break;
1✔
221
            }
222
        } while ($semiColon = $phpcsFile->findEndOfStatement($semiColon + 1));
1✔
223
    }
224

225
    private function removeUse(File $phpcsFile, int $from, int $to) : void
226
    {
227
        $tokens = $phpcsFile->getTokens();
1✔
228

229
        $phpcsFile->fixer->beginChangeset();
1✔
230

231
        // Remote whitespaces before in the same line
232
        if ($tokens[$from - 1]['code'] === T_WHITESPACE
1✔
233
            && $tokens[$from - 1]['line'] === $tokens[$from]['line']
1✔
234
            && $tokens[$from - 2]['line'] !== $tokens[$from]['line']
1✔
235
        ) {
236
            $phpcsFile->fixer->replaceToken($from - 1, '');
1✔
237
        }
238

239
        for ($i = $from; $i <= $to; ++$i) {
1✔
240
            $phpcsFile->fixer->replaceToken($i, '');
1✔
241
        }
242

243
        // Also remove whitespace after the semicolon (new lines).
244
        if (isset($tokens[$to + 1]) && $tokens[$to + 1]['code'] === T_WHITESPACE) {
1✔
245
            $phpcsFile->fixer->replaceToken($to + 1, '');
1✔
246
        }
247
        $phpcsFile->fixer->endChangeset();
1✔
248
    }
249

250
    private function isClassUsed(File $phpcsFile, int $usePtr, int $classPtr) : bool
251
    {
252
        $tokens = $phpcsFile->getTokens();
1✔
253

254
        // Search where the class name is used. PHP treats class names case
255
        // insensitive, that's why we cannot search for the exact class name string
256
        // and need to iterate over all T_STRING tokens in the file.
257
        $classUsed = $phpcsFile->findNext($this->checkInTokens, $classPtr + 1);
1✔
258
        $className = $tokens[$classPtr]['content'];
1✔
259

260
        // Check if the referenced class is in the same namespace as the current
261
        // file. If it is then the use statement is not necessary.
262
        $namespacePtr = $phpcsFile->findPrevious(T_NAMESPACE, $usePtr);
1✔
263

264
        $namespaceEnd = $namespacePtr !== false && isset($tokens[$namespacePtr]['scope_closer'])
1✔
265
            ? $tokens[$namespacePtr]['scope_closer']
1✔
266
            : null;
1✔
267

268
        $type = 'class';
1✔
269
        $next = $phpcsFile->findNext(Tokens::$emptyTokens, $usePtr + 1, null, true);
1✔
270
        if ($tokens[$next]['code'] === T_STRING
1✔
271
            && in_array(strtolower($tokens[$next]['content']), ['const', 'function'], true)
1✔
272
        ) {
273
            $type = strtolower($tokens[$next]['content']);
1✔
274
        }
275

276
        $searchName = $type === 'const' ? $className : strtolower($className);
1✔
277

278
        $prev = $phpcsFile->findPrevious(
1✔
279
            Tokens::$emptyTokens + [
1✔
280
                T_STRING => T_STRING,
1✔
281
                T_NS_SEPARATOR => T_NS_SEPARATOR,
1✔
282
            ],
1✔
283
            $classPtr - 1,
1✔
284
            null,
1✔
285
            $usePtr
1✔
286
        );
1✔
287

288
        // Only if alias is not used.
289
        if ($tokens[$prev]['code'] !== T_AS) {
1✔
290
            $isGroup = $tokens[$prev]['code'] === T_OPEN_USE_GROUP
1✔
291
                || $phpcsFile->findPrevious(T_OPEN_USE_GROUP, $prev, $usePtr) !== false;
1✔
292

293
            $useNamespace = '';
1✔
294
            if ($isGroup || $tokens[$prev]['code'] !== T_COMMA) {
1✔
295
                $useNamespacePtr = $type === 'class' ? $next : $next + 1;
1✔
296
                $useNamespace = $this->getNamespace(
1✔
297
                    $phpcsFile,
1✔
298
                    $useNamespacePtr,
1✔
299
                    [T_OPEN_USE_GROUP, T_COMMA, T_AS, T_SEMICOLON]
1✔
300
                );
1✔
301

302
                if ($isGroup) {
1✔
303
                    $useNamespace .= '\\';
1✔
304
                }
305
            }
306

307
            if ($tokens[$prev]['code'] === T_COMMA || $tokens[$prev]['code'] === T_OPEN_USE_GROUP) {
1✔
308
                $useNamespace .= $this->getNamespace(
1✔
309
                    $phpcsFile,
1✔
310
                    $prev + 1,
1✔
311
                    [T_CLOSE_USE_GROUP, T_COMMA, T_AS, T_SEMICOLON]
1✔
312
                );
1✔
313
            }
314

315
            $useNamespace = substr($useNamespace, 0, strrpos($useNamespace, '\\') ?: 0);
1✔
316

317
            if ($namespacePtr !== false) {
1✔
318
                $namespace = $this->getNamespace($phpcsFile, $namespacePtr + 1, [T_CURLY_OPEN, T_SEMICOLON]);
1✔
319

320
                if (strcasecmp($namespace, $useNamespace) === 0) {
1✔
321
                    $classUsed = false;
1✔
322
                }
323
            } elseif ($namespacePtr === false && $useNamespace === '') {
1✔
324
                $classUsed = false;
1✔
325
            }
326
        }
327

328
        $emptyTokens = Tokens::$emptyTokens;
1✔
329
        unset($emptyTokens[T_DOC_COMMENT_TAG]);
1✔
330

331
        while ($classUsed !== false) {
1✔
332
            $isStringToken = $tokens[$classUsed]['code'] === T_STRING;
1✔
333

334
            $match = null;
1✔
335

336
            if (($isStringToken
1✔
337
                    && (($type !== 'const' && strtolower($tokens[$classUsed]['content']) === $searchName)
1✔
338
                        || ($type === 'const' && $tokens[$classUsed]['content'] === $searchName)))
1✔
339
                || ($type === 'class'
1✔
340
                    && (($tokens[$classUsed]['code'] === T_DOC_COMMENT_STRING
1✔
341
                            && preg_match(
1✔
342
                                '/(\s|\||\(|^)' . preg_quote($searchName, '/') . '(\s|\||\\\\|$|\[\])/i',
1✔
343
                                $tokens[$classUsed]['content']
1✔
344
                            ))
1✔
345
                        || ($tokens[$classUsed]['code'] === T_DOC_COMMENT_TAG
1✔
346
                            && preg_match(
1✔
347
                                '/@' . preg_quote($searchName, '/') . '(\(|\\\\|$)/i',
1✔
348
                                $tokens[$classUsed]['content']
1✔
349
                            ))
1✔
350
                        || (! $isStringToken
1✔
351
                            && ! preg_match(
1✔
352
                                '/"[^"]*' . preg_quote($searchName, '/') . '\b[^"]*"/i',
1✔
353
                                $tokens[$classUsed]['content']
1✔
354
                            )
1✔
355
                            && preg_match(
1✔
356
                                '/(?<!")@' . preg_quote($searchName, '/') . '\b/i',
1✔
357
                                $tokens[$classUsed]['content'],
1✔
358
                                $match
1✔
359
                            ))))
1✔
360
            ) {
361
                $beforeUsage = $phpcsFile->findPrevious(
1✔
362
                    $isStringToken ? Tokens::$emptyTokens : $emptyTokens,
1✔
363
                    $classUsed - 1,
1✔
364
                    null,
1✔
365
                    true
1✔
366
                );
1✔
367

368
                if ($isStringToken) {
1✔
369
                    if ($this->determineType($phpcsFile, $beforeUsage, $classUsed) === $type) {
1✔
370
                        return true;
1✔
371
                    }
372
                } elseif ($tokens[$classUsed]['code'] === T_DOC_COMMENT_STRING) {
1✔
373
                    if ($tokens[$beforeUsage]['code'] === T_DOC_COMMENT_TAG
1✔
374
                        && in_array($tokens[$beforeUsage]['content'], CodingStandard::TAG_WITH_TYPE, true)
1✔
375
                    ) {
376
                        return true;
1✔
377
                    }
378

379
                    if ($match) {
1✔
380
                        return true;
1✔
381
                    }
382
                } else {
383
                    return true;
1✔
384
                }
385
            }
386

387
            $classUsed = $phpcsFile->findNext($this->checkInTokens, $classUsed + 1, $namespaceEnd);
1✔
388
        }
389

390
        return false;
1✔
391
    }
392

393
    private function determineType(File $phpcsFile, int $beforePtr, int $ptr) : ?string
394
    {
395
        $tokens = $phpcsFile->getTokens();
1✔
396

397
        $beforeCode = $tokens[$beforePtr]['code'];
1✔
398

399
        if (in_array($beforeCode, [
1✔
400
            T_NS_SEPARATOR,
1✔
401
            T_OBJECT_OPERATOR,
1✔
402
            T_DOUBLE_COLON,
1✔
403
            T_FUNCTION,
1✔
404
            T_CONST,
1✔
405
            T_AS,
1✔
406
            T_INSTEADOF,
1✔
407
        ], true)) {
1✔
408
            return null;
1✔
409
        }
410

411
        if (in_array($beforeCode, [
1✔
412
            T_NEW,
1✔
413
            T_NULLABLE,
1✔
414
            T_EXTENDS,
1✔
415
            T_IMPLEMENTS,
1✔
416
            T_INSTANCEOF,
1✔
417
        ], true)) {
1✔
418
            return 'class';
1✔
419
        }
420

421
        if ($beforeCode === T_COMMA) {
1✔
422
            $prev = $phpcsFile->findPrevious(
1✔
423
                Tokens::$emptyTokens + [
1✔
424
                    T_STRING => T_STRING,
1✔
425
                    T_NS_SEPARATOR => T_NS_SEPARATOR,
1✔
426
                    T_COMMA => T_COMMA,
1✔
427
                ],
1✔
428
                $beforePtr - 1,
1✔
429
                null,
1✔
430
                true
1✔
431
            );
1✔
432

433
            if ($tokens[$prev]['code'] === T_IMPLEMENTS || $tokens[$prev]['code'] === T_EXTENDS) {
1✔
434
                return 'class';
1✔
435
            }
436

437
            if ($tokens[$prev]['code'] === T_INSTEADOF) {
1✔
438
                return null;
×
439
            }
440

441
            if ($tokens[$prev]['code'] === T_USE) {
1✔
442
                $beforeCode = T_USE;
1✔
443
            }
444
        }
445

446
        // Trait usage
447
        if ($beforeCode === T_USE) {
1✔
448
            if (CodingStandard::isTraitUse($phpcsFile, $beforePtr)) {
1✔
449
                return 'class';
1✔
450
            }
451

452
            return null;
1✔
453
        }
454

455
        $afterPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $ptr + 1, null, true);
1✔
456
        $afterCode = $tokens[$afterPtr]['code'];
1✔
457

458
        if ($afterCode === T_AS) {
1✔
459
            return null;
1✔
460
        }
461

462
        if ($afterCode === T_OPEN_PARENTHESIS) {
1✔
463
            return 'function';
1✔
464
        }
465

466
        if (in_array($afterCode, [
1✔
467
            T_DOUBLE_COLON,
1✔
468
            T_VARIABLE,
1✔
469
            T_ELLIPSIS,
1✔
470
            T_NS_SEPARATOR,
1✔
471
            T_OPEN_CURLY_BRACKET,
1✔
472
        ], true)) {
1✔
473
            return 'class';
1✔
474
        }
475

476
        if ($beforeCode === T_COLON) {
1✔
477
            $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $beforePtr - 1, null, true);
1✔
478
            if ($prev !== false
1✔
479
                && $tokens[$prev]['code'] === T_CLOSE_PARENTHESIS
1✔
480
                && isset($tokens[$prev]['parenthesis_owner'])
1✔
481
                && $tokens[$tokens[$prev]['parenthesis_owner']]['code'] === T_FUNCTION
1✔
482
            ) {
483
                return 'class';
1✔
484
            }
485
        }
486

487
        if ($afterCode === T_BITWISE_OR) {
1✔
488
            $prev = $phpcsFile->findPrevious(
1✔
489
                Tokens::$emptyTokens + [
1✔
490
                    T_BITWISE_OR => T_BITWISE_OR,
1✔
491
                    T_STRING => T_STRING,
1✔
492
                    T_NS_SEPARATOR => T_NS_SEPARATOR,
1✔
493
                    T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS,
1✔
494
                ],
1✔
495
                $afterPtr,
1✔
496
                null,
1✔
497
                true
1✔
498
            );
1✔
499

500
            if ($tokens[$prev]['code'] === T_CATCH) {
1✔
501
                return 'class';
1✔
502
            }
503
        }
504

505
        return 'const';
1✔
506
    }
507

508
    private function getNamespace(File $phpcsFile, int $ptr, array $stop) : string
509
    {
510
        $tokens = $phpcsFile->getTokens();
1✔
511

512
        $result = '';
1✔
513
        while (! in_array($tokens[$ptr]['code'], $stop, true)) {
1✔
514
            if (in_array($tokens[$ptr]['code'], [T_STRING, T_NS_SEPARATOR], true)) {
1✔
515
                $result .= $tokens[$ptr]['content'];
1✔
516
            }
517

518
            ++$ptr;
1✔
519
        }
520

521
        return trim(trim($result), '\\');
1✔
522
    }
523
}
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

© 2025 Coveralls, Inc