Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHPCSUtils / 790

13 Sep 2020 - 12:41 coverage: 97.485% (-0.3%) from 97.777%
790

Pull #212

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Merge 4444dc926 into f4d902b02
Pull Request #212: UseStatements::splitImportUseStatement(): compatibility with the PHP 8 identifier name tokenization

20 of 27 new or added lines in 1 file covered. (74.07%)

1 existing line in 1 file now uncovered.

2558 of 2624 relevant lines covered (97.48%)

77.19 hits per line

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

93.62
/PHPCSUtils/Utils/UseStatements.php
1
<?php
2
/**
3
 * PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4
 *
5
 * @package   PHPCSUtils
6
 * @copyright 2019-2020 PHPCSUtils Contributors
7
 * @license   https://opensource.org/licenses/LGPL-3.0 LGPL3
8
 * @link      https://github.com/PHPCSStandards/PHPCSUtils
9
 */
10

11
namespace PHPCSUtils\Utils;
12

13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Files\File;
15
use PHP_CodeSniffer\Util\Tokens;
16
use PHPCSUtils\BackCompat\BCTokens;
17
use PHPCSUtils\BackCompat\Helper;
18
use PHPCSUtils\Utils\Conditions;
19
use PHPCSUtils\Utils\Parentheses;
20

21
/**
22
 * Utility functions for examining use statements.
23
 *
24
 * @since 1.0.0
25
 */
26
class UseStatements
27
{
28

29
    /**
30
     * Determine what a T_USE token is used for.
31
     *
32
     * @since 1.0.0
33
     *
34
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
35
     * @param int                         $stackPtr  The position of the `T_USE` token.
36
     *
37
     * @return string Either `'closure'`, `'import'` or `'trait'`.
38
     *                An empty string will be returned if the token is used in an
39
     *                invalid context or if it couldn't be reliably determined what
40
     *                the `T_USE` token is used for. An empty string being returned will
41
     *                normally mean the code being examined contains a parse error.
42
     *
43
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
44
     *                                                      `T_USE` token.
45
     */
46
    public static function getType(File $phpcsFile, $stackPtr)
47
    {
48
        $tokens = $phpcsFile->getTokens();
224×
49

50
        if (isset($tokens[$stackPtr]) === false
224×
51
            || $tokens[$stackPtr]['code'] !== \T_USE
224×
52
        ) {
53
            throw new RuntimeException('$stackPtr must be of type T_USE');
8×
54
        }
55

56
        $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
216×
57
        if ($next === false) {
216×
58
            // Live coding or parse error.
59
            return '';
12×
60
        }
61

62
        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
204×
63
        if ($prev !== false && $tokens[$prev]['code'] === \T_CLOSE_PARENTHESIS
204×
64
            && Parentheses::isOwnerIn($phpcsFile, $prev, \T_CLOSURE) === true
204×
65
        ) {
66
            return 'closure';
36×
67
        }
68

69
        $lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr);
168×
70
        if (($tokens[$lastCondition]['code'] === \T_CASE
168×
71
                || $tokens[$lastCondition]['code'] === \T_DEFAULT)
168×
72
            && \version_compare(Helper::getVersion(), '2.99.99', '<') === true
168×
73
            && Conditions::hasCondition($phpcsFile, $stackPtr, [\T_SWITCH]) === false
168×
74
        ) {
75
            $lastCondition = Conditions::getLastCondition($phpcsFile, $lastCondition);
!
76
        }
77

78
        if ($lastCondition === false || $tokens[$lastCondition]['code'] === \T_NAMESPACE) {
168×
79
            // Global or scoped namespace and not a closure use statement.
80
            return 'import';
84×
81
        }
82

83
        $traitScopes = BCTokens::ooScopeTokens();
84×
84
        // Only classes and traits can import traits.
85
        unset($traitScopes[\T_INTERFACE]);
84×
86

87
        if (isset($traitScopes[$tokens[$lastCondition]['code']]) === true) {
84×
88
            return 'trait';
72×
89
        }
90

91
        return '';
12×
92
    }
93

94
    /**
95
     * Determine whether a T_USE token represents a closure use statement.
96
     *
97
     * @since 1.0.0
98
     *
99
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
100
     * @param int                         $stackPtr  The position of the `T_USE` token.
101
     *
102
     * @return bool `TRUE` if the token passed is a closure use statement.
103
     *              `FALSE` if it's not.
104
     *
105
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
106
     *                                                      `T_USE` token.
107
     */
108
    public static function isClosureUse(File $phpcsFile, $stackPtr)
109
    {
110
        return (self::getType($phpcsFile, $stackPtr) === 'closure');
72×
111
    }
112

113
    /**
114
     * Determine whether a T_USE token represents a class/function/constant import use statement.
115
     *
116
     * @since 1.0.0
117
     *
118
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
119
     * @param int                         $stackPtr  The position of the `T_USE` token.
120
     *
121
     * @return bool `TRUE` if the token passed is an import use statement.
122
     *              `FALSE` if it's not.
123
     *
124
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
125
     *                                                      `T_USE` token.
126
     */
127
    public static function isImportUse(File $phpcsFile, $stackPtr)
128
    {
129
        return (self::getType($phpcsFile, $stackPtr) === 'import');
72×
130
    }
131

132
    /**
133
     * Determine whether a T_USE token represents a trait use statement.
134
     *
135
     * @since 1.0.0
136
     *
137
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
138
     * @param int                         $stackPtr  The position of the `T_USE` token.
139
     *
140
     * @return bool `TRUE` if the token passed is a trait use statement.
141
     *              `FALSE` if it's not.
142
     *
143
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
144
     *                                                      `T_USE` token.
145
     */
146
    public static function isTraitUse(File $phpcsFile, $stackPtr)
147
    {
148
        return (self::getType($phpcsFile, $stackPtr) === 'trait');
72×
149
    }
150

151
    /**
152
     * Split an import use statement into individual imports.
153
     *
154
     * Handles single import, multi-import and group-import use statements.
155
     *
156
     * @since 1.0.0
157
     * @since 1.0.0-alpha4 Added support for PHP 8.0 identifier name tokenization.
158
     *
159
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found.
160
     * @param int                         $stackPtr  The position in the stack of the `T_USE` token.
161
     *
162
     * @return array A multi-level array containing information about the use statement.
163
     *               The first level is `'name'`, `'function'` and `'const'`. These keys will always exist.
164
     *               If any statements are found for any of these categories, the second level
165
     *               will contain the alias/name as the key and the full original use name as the
166
     *               value for each of the found imports or an empty array if no imports were found
167
     *               in this use statement for a particular category.
168
     *
169
     *               For example, for this function group use statement:
170
     *               ```php
171
     *               use function Vendor\Package\{
172
     *                   LevelA\Name as Alias,
173
     *                   LevelB\Another_Name,
174
     *               };
175
     *               ```
176
     *               the return value would look like this:
177
     *               ```php
178
     *               array(
179
     *                 'name'     => array(),
180
     *                 'function' => array(
181
     *                   'Alias'        => 'Vendor\Package\LevelA\Name',
182
     *                   'Another_Name' => 'Vendor\Package\LevelB\Another_Name',
183
     *                 ),
184
     *                 'const'    => array(),
185
     *               )
186
     *               ```
187
     *
188
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
189
     *                                                      `T_USE` token.
190
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `T_USE` token is not for an import
191
     *                                                      use statement.
192
     */
193
    public static function splitImportUseStatement(File $phpcsFile, $stackPtr)
194
    {
195
        $tokens = $phpcsFile->getTokens();
92×
196

197
        if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_USE) {
92×
198
            throw new RuntimeException('$stackPtr must be of type T_USE');
8×
199
        }
200

201
        if (self::isImportUse($phpcsFile, $stackPtr) === false) {
84×
202
            throw new RuntimeException('$stackPtr must be an import use statement');
8×
203
        }
204

205
        $statements = [
206
            'name'     => [],
76×
207
            'function' => [],
208
            'const'    => [],
209
        ];
210

211
        $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
76×
212
        if ($endOfStatement === false) {
76×
213
            // Live coding or parse error.
214
            return $statements;
8×
215
        }
216

217
        $endOfStatement++;
68×
218

219
        $start     = true;
68×
220
        $useGroup  = false;
68×
221
        $hasAlias  = false;
68×
222
        $baseName  = '';
68×
223
        $name      = '';
68×
224
        $type      = '';
68×
225
        $fixedType = false;
68×
226
        $alias     = '';
68×
227

228
        for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
68×
229
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
68×
230
                continue;
68×
231
            }
232

233
            $tokenType = $tokens[$i]['type'];
68×
234

235
            /*
236
             * BC: Work round a tokenizer bug related to a parse error.
237
             *
238
             * If `function` or `const` is used as the alias, the semi-colon after it would
239
             * be tokenized as T_STRING.
240
             * For `function` this was fixed in PHPCS 2.8.0. For `const` the issue still exists
241
             * in PHPCS 3.5.2.
242
             *
243
             * Along the same lines, the `}` T_CLOSE_USE_GROUP would also be tokenized as T_STRING.
244
             */
245
            if ($tokenType === 'T_STRING') {
68×
246
                if ($tokens[$i]['content'] === ';') {
68×
NEW
247
                    $tokenType = 'T_SEMICOLON';
!
248
                } elseif ($tokens[$i]['content'] === '}') {
68×
249
                    $tokenType = 'T_CLOSE_USE_GROUP';
1×
250
                }
251
            }
252

253
            switch ($tokenType) {
254
                case 'T_STRING':
68×
255
                    // Only when either at the start of the statement or at the start of a new sub within a group.
256
                    if ($start === true && $fixedType === false) {
68×
257
                        $content = \strtolower($tokens[$i]['content']);
64×
258
                        if ($content === 'function'
259
                            || $content === 'const'
64×
260
                        ) {
261
                            $type  = $content;
38×
262
                            $start = false;
38×
263
                            if ($useGroup === false) {
38×
264
                                $fixedType = true;
36×
265
                            }
266

267
                            break;
38×
268
                        } else {
269
                            $type = 'name';
28×
270
                        }
271
                    }
272

273
                    $start = false;
68×
274

275
                    if ($hasAlias === false) {
68×
276
                        $name .= $tokens[$i]['content'];
68×
277
                    }
278

279
                    $alias = $tokens[$i]['content'];
68×
280

281
                    /*
282
                     * BC: work around PHPCS tokenizer issue in PHPCS < 3.5.7 where anything directly after
283
                     * a `function` or `const` keyword would be retokenized to `T_STRING`, including the
284
                     * PHP 8 identifier name tokens.
285
                     */
286
                    $hasSlash = \strrpos($tokens[$i]['content'], '\\');
68×
287
                    if ($hasSlash !== false) {
68×
288
                        $alias = \substr($tokens[$i]['content'], ($hasSlash + 1));
4×
289
                    }
290

291
                    break;
68×
292

293
                case 'T_NAME_QUALIFIED':
68×
294
                case 'T_NAME_FULLY_QUALIFIED': // This would be a parse error, but handle it anyway.
68×
295
                    // Only when either at the start of the statement or at the start of a new sub within a group.
NEW
296
                    if ($start === true && $fixedType === false) {
!
NEW
297
                        $type = 'name';
!
298
                    }
299

NEW
300
                    $start = false;
!
301

NEW
302
                    if ($hasAlias === false) {
!
NEW
303
                        $name .= $tokens[$i]['content'];
!
304
                    }
305

NEW
306
                    $alias = \substr($tokens[$i]['content'], (\strrpos($tokens[$i]['content'], '\\') + 1));
!
UNCOV
307
                    break;
!
308

309
                case 'T_AS':
68×
310
                    $hasAlias = true;
48×
311
                    break;
48×
312

313
                case 'T_OPEN_USE_GROUP':
68×
314
                    $start    = true;
20×
315
                    $useGroup = true;
20×
316
                    $baseName = $name;
20×
317
                    $name     = '';
20×
318
                    break;
20×
319

320
                case 'T_SEMICOLON':
68×
321
                case 'T_CLOSE_TAG':
68×
322
                case 'T_CLOSE_USE_GROUP':
68×
323
                case 'T_COMMA':
68×
324
                    if ($name !== '') {
68×
325
                        if ($useGroup === true) {
68×
326
                            $statements[$type][$alias] = $baseName . $name;
20×
327
                        } else {
328
                            $statements[$type][$alias] = $name;
48×
329
                        }
330
                    }
331

332
                    if ($tokenType !== 'T_COMMA') {
68×
333
                        break 2;
68×
334
                    }
335

336
                    // Reset.
337
                    $start    = true;
28×
338
                    $name     = '';
28×
339
                    $hasAlias = false;
28×
340
                    if ($fixedType === false) {
28×
341
                        $type = '';
12×
342
                    }
343
                    break;
28×
344

345
                case 'T_NS_SEPARATOR':
68×
346
                    $name .= $tokens[$i]['content'];
68×
347
                    break;
68×
348

349
                case 'T_FUNCTION':
20×
350
                case 'T_CONST':
16×
351
                    /*
352
                     * BC: Work around tokenizer bug in PHPCS < 3.4.1.
353
                     *
354
                     * `function`/`const` in `use function`/`use const` tokenized as T_FUNCTION/T_CONST
355
                     * instead of T_STRING when there is a comment between the keywords.
356
                     *
357
                     * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/2431
358
                     */
359
                    if ($start === true && $fixedType === false) {
12×
360
                        $type  = \strtolower($tokens[$i]['content']);
6×
361
                        $start = false;
6×
362
                        if ($useGroup === false) {
6×
363
                            $fixedType = true;
4×
364
                        }
365

366
                        break;
6×
367
                    }
368

369
                    $start = false;
6×
370

371
                    if ($hasAlias === false) {
6×
372
                        $name .= $tokens[$i]['content'];
4×
373
                    }
374

375
                    $alias = $tokens[$i]['content'];
6×
376
                    break;
6×
377

378
                /*
379
                 * Fall back in case reserved keyword is (illegally) used in name.
380
                 * Parse error, but not our concern.
381
                 */
382
                default:
383
                    if ($hasAlias === false) {
8×
384
                        $name .= $tokens[$i]['content'];
4×
385
                    }
386

387
                    $alias = $tokens[$i]['content'];
8×
388
                    break;
8×
389
            }
390
        }
391

392
        return $statements;
68×
393
    }
394

395
    /**
396
     * Split an import use statement into individual imports and merge it with an array of previously
397
     * seen import use statements.
398
     *
399
     * Beware: this method should only be used to combine the import use statements found in *one* file.
400
     * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
401
     *
402
     * In most cases when tempted to use this method, the {@see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff}
403
     * (upcoming) should be used instead.
404
     *
405
     * @see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff
406
     * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
407
     * @see \PHPCSUtils\Utils\UseStatements::mergeImportUseStatements()
408
     *
409
     * @since 1.0.0-alpha3
410
     *
411
     * @param \PHP_CodeSniffer\Files\File $phpcsFile             The file where this token was found.
412
     * @param int                         $stackPtr              The position in the stack of the `T_USE` token.
413
     * @param array                       $previousUseStatements The import `use` statements collected so far.
414
     *                                                           This should be either the output of a
415
     *                                                           previous call to this method or the output of
416
     *                                                           an earlier call to the
417
     *                                                           {@see UseStatements::splitImportUseStatement()}
418
     *                                                           method.
419
     *
420
     * @return array A multi-level array containing information about the current `use` statement combined with
421
     *               the previously collected `use` statement information.
422
     *               See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
423
     */
424
    public static function splitAndMergeImportUseStatement(File $phpcsFile, $stackPtr, $previousUseStatements)
425
    {
426
        try {
427
            $useStatements         = self::splitImportUseStatement($phpcsFile, $stackPtr);
20×
428
            $previousUseStatements = self::mergeImportUseStatements($previousUseStatements, $useStatements);
16×
429
        } catch (RuntimeException $e) {
12×
430
            // Not an import use statement.
431
        }
432

433
        return $previousUseStatements;
20×
434
    }
435

436
    /**
437
     * Merge two import use statement arrays.
438
     *
439
     * Beware: this method should only be used to combine the import use statements found in *one* file.
440
     * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable.
441
     *
442
     * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement()
443
     *
444
     * @since 1.0.0-alpha4
445
     *
446
     * @param array $previousUseStatements The import `use` statements collected so far.
447
     *                                     This should be either the output of a
448
     *                                     previous call to this method or the output of
449
     *                                     an earlier call to the
450
     *                                     {@see UseStatements::splitImportUseStatement()}
451
     *                                     method.
452
     * @param array $currentUseStatement   The parsed import `use` statements to merge with
453
     *                                     the previously collected use statements.
454
     *                                     This should be the output of a call to the
455
     *                                     {@see UseStatements::splitImportUseStatement()}
456
     *                                     method.
457
     *
458
     * @return array A multi-level array containing information about the current `use` statement combined with
459
     *               the previously collected `use` statement information.
460
     *               See {@see UseStatements::splitImportUseStatement()} for more details about the array format.
461
     */
462
    public static function mergeImportUseStatements($previousUseStatements, $currentUseStatement)
463
    {
464
        if (isset($previousUseStatements['name']) === false) {
16×
465
            $previousUseStatements['name'] = $currentUseStatement['name'];
4×
466
        } else {
467
            $previousUseStatements['name'] += $currentUseStatement['name'];
12×
468
        }
469
        if (isset($previousUseStatements['function']) === false) {
16×
470
            $previousUseStatements['function'] = $currentUseStatement['function'];
4×
471
        } else {
472
            $previousUseStatements['function'] += $currentUseStatement['function'];
12×
473
        }
474
        if (isset($previousUseStatements['const']) === false) {
16×
475
            $previousUseStatements['const'] = $currentUseStatement['const'];
4×
476
        } else {
477
            $previousUseStatements['const'] += $currentUseStatement['const'];
12×
478
        }
479

480
        return $previousUseStatements;
16×
481
    }
482
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc